diff --git a/README.mkd b/README.mkd index 83f10c2..978a277 100644 --- a/README.mkd +++ b/README.mkd @@ -1,12 +1,14 @@ ## vim-gitgutter -A Vim plugin which shows a git diff in the 'gutter' (sign column). It shows whether each line has been added, modified, and where lines have been removed. You can also stage and undo individual hunks. +A Vim plugin which shows a git diff in the 'gutter' (sign column). It shows which lines have been added, modified, or removed. You can also preview, stage, and undo individual hunks. + +The signs are always up to date and the plugin never saves your buffer. Features: * Shows signs for added, modified, and removed lines. -* Runs the diffs asynchronously in terminal Vim/MacVim (7.4.1826+), gVim (7.4.1850+), MacVim GUI (7.4.1832+), and NeoVim. -* Ensures signs are always as up to date as possible (but without running more than necessary). +* Runs the diffs asynchronously where possible. +* Ensures signs are always up to date. * Quick jumping between blocks of changed lines ("hunks"). * Stage/undo/preview individual hunks. * Provides a hunk text object. @@ -39,7 +41,7 @@ In the screenshot above you can see: ### Installation -Before installation, please check your Vim supports signs by running `:echo has('signs')`. `1` means you're all set; `0` means you need to install a Vim with signs support. If you're compiling Vim yourself you need the 'big' or 'huge' feature set. [MacVim][] supports signs. +Before installation, please check your Vim supports signs by running `:echo has('signs')`. `1` means you're all set; `0` means you need to install a Vim with signs support. If you're compiling Vim yourself you need the 'big' or 'huge' feature set. MacVim supports signs. You install vim-gitgutter like any other vim plugin. @@ -100,12 +102,9 @@ cp -r vim-gitgutter/* ~/.vim/ See `:help add-global-plugin`. -If you are on Windows you may find the command prompt pops up briefly every time vim-gitgutter runs. You can avoid this by installing both [vim-misc](https://github.com/xolox/vim-misc) and [vim-shell](https://github.com/xolox/vim-shell). If you have those two plugins but don't want vim-gitgutter to use them, you can opt out with `let g:gitgutter_avoid_cmd_prompt_on_windows = 0` in your `~/.vimrc`. - - ### Getting started -When you make a change to a file tracked by git, the diff markers should appear automatically. The delay is governed by vim's `updatetime` option; the default value is 4 seconds but I suggest reducing it to around 250ms (add `set updatetime=250` to your vimrc). +When you make a change to a file tracked by git, the diff markers should appear automatically. The delay is governed by vim's `updatetime` option; the default value is `4000`, i.e. 4 seconds, but I suggest reducing it to around 100ms (add `set updatetime=100` to your vimrc). You can jump between hunks with `[c` and `]c`. You can preview, stage, and undo hunks with `hp`, `hs`, and `hu` respectively. @@ -136,7 +135,7 @@ Note that if you have line highlighting on and signs off, you will have an empty If you switch off both line highlighting and signs, you won't see the sign column. That is unless you configure the sign column always to be there (see Sign Column section). -To keep your Vim snappy, vim-gitgutter will suppress itself when a file has more than 500 changes. As soon as the number of changes falls below the limit vim-gitgutter will show the signs again. You can configure the threshold with: +To keep your Vim snappy, vim-gitgutter will suppress the signs when a file has more than 500 changes. As soon as the number of changes falls below the limit vim-gitgutter will show the signs again. You can configure the threshold with: ```viml let g:gitgutter_max_signs = 500 " default value @@ -205,33 +204,6 @@ Finally, you can force vim-gitgutter to update its signs across all visible buff See the customisation section below for how to change the defaults. -### When are the signs updated? - -By default the signs are updated as follows: - -| Event | Reason for update | Configuration | -|---------------------------|--------------------------------------|------------------------| -| Stop typing | So the signs are real time | `g:gitgutter_realtime` | -| Switch buffer | To notice change to git index | `g:gitgutter_eager` | -| Switch tab | To notice change to git index | `g:gitgutter_eager` | -| Focus the GUI | To notice change to git index | `g:gitgutter_eager` (not gVim on Windows) | -| After shell command | To notice change to git index | `g:gitgutter_eager` | -| Read a file into a buffer | To display initial signs | [always] | -| Save a buffer | So non-realtime signs are up to date | [always] | -| Change a file outside Vim | To notice `git stash` | [always] | - -The length of time Vim waits after you stop typing before it triggers the plugin is governed by the setting `updatetime`. This defaults to `4000` milliseconds which is rather too long. I recommend around `250` milliseconds but it depends on your system and your preferences. Note that in terminal Vim pre-7.4.427 an `updatetime` of less than approximately `1000` milliseconds can lead to random highlighting glitches; the lower the `updatetime`, the more glitches. - -If you experience a lag, you can trade speed for accuracy: - -```viml -let g:gitgutter_realtime = 0 -let g:gitgutter_eager = 0 -``` - -Note the realtime updating requires Vim 7.3.105 or higher. - - ### Customisation You can customise: @@ -351,7 +323,7 @@ If you use an alternative to grep, you can tell vim-gitgutter to use it here. ```viml " Default: -let g:gitgutter_grep_command = 'grep' +let g:gitgutter_grep = 'grep' ``` #### To turn off vim-gitgutter by default @@ -498,13 +470,13 @@ Unstaging staged hunks is feasible but not quite as easy as it sounds. There ar 2. The version staged in the index. 3. The version in the working tree, in your vim buffer. -`git-diff` without arguments shows you how 3 and 2 differ; this is what vim-gitgutter shows too. +`git-diff` without arguments shows you how 2 and 3 differ; this is what vim-gitgutter shows too. -`git-diff --staged` shows you how 2 and 1 differ. +`git-diff --staged` shows you how 1 and 2 differ. -Let's say you are looking at a file in vim which has some unstaged changes. Now you stage a hunk, either via vim-gitgutter or another means. The hunk is no longer marked in vim-gitgutter because it is the same in 3 and 2. +Let's say you are looking at a file in vim which has some unstaged changes. Now you stage a hunk, either via vim-gitgutter or another means. The hunk is no longer marked in vim-gitgutter because it is the same in 2 and 3. -Now you want to unstage that hunk. To see it, you need the difference between 2 and 1. For vim-gitgutter to show those differences, it would need to show you 2 instead of 3 in your vim buffer. But 2 is virtual so vim-gitgutter would need to handle it without touching 3. +Now you want to unstage that hunk. To see it, you need the difference between 1 and 2. For vim-gitgutter to show those differences, it would need to show you 2 instead of 3 in your vim buffer. But 2 is virtual so vim-gitgutter would need to handle it without touching 3. I intend to implement this but I can't commit to any deadline. @@ -512,32 +484,24 @@ I intend to implement this but I can't commit to any deadline. Your colorscheme is configuring the `SignColumn` highlight group weirdly. Please see the section above on customising the sign column. -> There's a noticeable lag when vim-gitter runs; how can I avoid it? - -By default vim-gitgutter runs often so the signs are as accurate as possible. The delay is governed by `updatetime`; see [above](#when-are-the-signs-updated) for more information. - -If you don't want realtime updates and would like to trade a little accuracy for speed, add this to your `~/.vimrc`: - -```viml -let g:gitgutter_realtime = 0 -let g:gitgutter_eager = 0 -``` - > What happens if I also use another plugin which uses signs (e.g. Syntastic)? Vim only allows one sign per line. Before adding a sign to a line, vim-gitgutter checks whether a sign has already been added by somebody else. If so it doesn't do anything. In other words vim-gitgutter won't overwrite another plugin's signs. It also won't remove another plugin's signs. -> Why aren't any signs showing at all? + +### Troubleshooting + +#### When no signs are showing at all Here are some things you can check: -* `:echo system("git --version")` succeeds. -* Your git config is compatible with the version of git returned by the command above. -* Your Vim supports signs (`:echo has('signs')` should give `1`). -* Your file is being tracked by git and has unstaged changes. -* If you have aliased or configured `grep` to use any flags, add `let g:gitgutter_grep_command = 'grep'` to your `~/.vimrc`. +* Try adding `let g:gitgutter_grep=''` to your vimrc. If it works, the problem is grep producing non-plain output; e.g. ANSI escape codes or colours. +* Verify `:echo system("git --version")` succeeds. +* Verify your git config is compatible with the version of git returned by the command above. +* Verify your Vim supports signs (`:echo has('signs')` should give `1`). +* Verify your file is being tracked by git and has unstaged changes. -> Why is the whole file marked as added when I edit it? +#### When the whole file is marked as added * If you use zsh, and you set `CDPATH`, make sure `CDPATH` doesn't include the current directory. @@ -561,4 +525,3 @@ Copyright Andrew Stewart, AirBlade Software Ltd. Released under the MIT licence [pathogen]: https://github.com/tpope/vim-pathogen [siv]: http://pluralsight.com/training/Courses/TableOfContents/smash-into-vim [airblade]: http://airbladesoftware.com/peepcode-vim - [macvim]: http://code.google.com/p/macvim/ diff --git a/autoload/gitgutter.vim b/autoload/gitgutter.vim index 6e8ca57..3b79b7d 100644 --- a/autoload/gitgutter.vim +++ b/autoload/gitgutter.vim @@ -1,67 +1,50 @@ -let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '' : '' - " Primary functions {{{ -function! gitgutter#all() abort - for buffer_id in gitgutter#utility#dedup(tabpagebuflist()) - let file = expand('#' . buffer_id . ':p') +function! gitgutter#all(force) abort + for bufnr in tabpagebuflist() + let file = expand('#'.bufnr.':p') if !empty(file) - call gitgutter#process_buffer(buffer_id, 0) + call gitgutter#init_buffer(bufnr) + call gitgutter#process_buffer(bufnr, a:force) endif endfor endfunction -" bufnr: (integer) the buffer to process. -" realtime: (boolean) when truthy, do a realtime diff; otherwise do a disk-based diff. -function! gitgutter#process_buffer(bufnr, realtime) abort - call gitgutter#utility#use_known_shell() - call gitgutter#utility#set_buffer(a:bufnr) - if gitgutter#utility#is_active() - if g:gitgutter_sign_column_always - call gitgutter#sign#add_dummy_sign() - endif - try - if !a:realtime || gitgutter#utility#has_fresh_changes() - let diff = gitgutter#diff#run_diff(a:realtime || gitgutter#utility#has_unsaved_changes(), 0) - if diff != 'async' - call gitgutter#handle_diff(diff) - endif +" Finds the file's path relative to the repo root. +function! gitgutter#init_buffer(bufnr) + let p = gitgutter#utility#repo_path(a:bufnr, 0) + if type(p) != v:t_string || empty(p) + call gitgutter#utility#set_repo_path(a:bufnr) + endif +endfunction + + +function! gitgutter#process_buffer(bufnr, force) abort + " NOTE a:bufnr is not necessarily the current buffer. + + if gitgutter#utility#is_active(a:bufnr) + if a:force || s:has_fresh_changes(a:bufnr) + + let diff = '' + try + let diff = gitgutter#diff#run_diff(a:bufnr, 0) + catch /gitgutter not tracked/ + call gitgutter#debug#log('Not tracked: '.gitgutter#utility#file(a:bufnr)) + catch /gitgutter diff failed/ + call gitgutter#debug#log('Diff failed: '.gitgutter#utility#file(a:bufnr)) + call gitgutter#hunk#reset(a:bufnr) + endtry + + if diff != 'async' + call gitgutter#diff#handler(a:bufnr, diff) endif - catch /diff failed/ - call gitgutter#debug#log('diff failed') - call gitgutter#hunk#reset() - endtry - execute "silent doautocmd" s:nomodeline "User GitGutter" - else - call gitgutter#hunk#reset() - endif - call gitgutter#utility#restore_shell() + endif + endif endfunction -function! gitgutter#handle_diff(diff) abort - call gitgutter#debug#log(a:diff) - - call gitgutter#utility#setbufvar(gitgutter#utility#bufnr(), 'tracked', 1) - - call gitgutter#hunk#set_hunks(gitgutter#diff#parse_diff(a:diff)) - let modified_lines = gitgutter#diff#process_hunks(gitgutter#hunk#hunks()) - - if len(modified_lines) > g:gitgutter_max_signs - call gitgutter#utility#warn_once('exceeded maximum number of signs (configured by g:gitgutter_max_signs).', 'max_signs') - call gitgutter#sign#clear_signs() - return - endif - - if g:gitgutter_signs || g:gitgutter_highlight_lines - call gitgutter#sign#update_signs(modified_lines) - endif - - call gitgutter#utility#save_last_seen_change() -endfunction - function! gitgutter#disable() abort " get list of all buffers (across all tabs) let buflist = [] @@ -69,13 +52,12 @@ function! gitgutter#disable() abort call extend(buflist, tabpagebuflist(i + 1)) endfor - for buffer_id in gitgutter#utility#dedup(buflist) - let file = expand('#' . buffer_id . ':p') + for bufnr in buflist + let file = expand('#'.bufnr.':p') if !empty(file) - call gitgutter#utility#set_buffer(buffer_id) - call gitgutter#sign#clear_signs() - call gitgutter#sign#remove_dummy_sign(1) - call gitgutter#hunk#reset() + call gitgutter#sign#clear_signs(bufnr) + call gitgutter#sign#remove_dummy_sign(bufnr, 1) + call gitgutter#hunk#reset(bufnr) endif endfor @@ -84,7 +66,7 @@ endfunction function! gitgutter#enable() abort let g:gitgutter_enabled = 1 - call gitgutter#all() + call gitgutter#all(1) endfunction function! gitgutter#toggle() abort @@ -97,163 +79,7 @@ endfunction " }}} -" Line highlights {{{ - -function! gitgutter#line_highlights_disable() abort - let g:gitgutter_highlight_lines = 0 - call gitgutter#highlight#define_sign_line_highlights() - - if !g:gitgutter_signs - call gitgutter#sign#clear_signs() - call gitgutter#sign#remove_dummy_sign(0) - endif - - redraw! +function! s:has_fresh_changes(bufnr) abort + return getbufvar(a:bufnr, 'changedtick') != gitgutter#utility#getbufvar(a:bufnr, 'tick') endfunction -function! gitgutter#line_highlights_enable() abort - let old_highlight_lines = g:gitgutter_highlight_lines - - let g:gitgutter_highlight_lines = 1 - call gitgutter#highlight#define_sign_line_highlights() - - if !old_highlight_lines && !g:gitgutter_signs - call gitgutter#all() - endif - - redraw! -endfunction - -function! gitgutter#line_highlights_toggle() abort - if g:gitgutter_highlight_lines - call gitgutter#line_highlights_disable() - else - call gitgutter#line_highlights_enable() - endif -endfunction - -" }}} - -" Signs {{{ - -function! gitgutter#signs_enable() abort - let old_signs = g:gitgutter_signs - - let g:gitgutter_signs = 1 - call gitgutter#highlight#define_sign_text_highlights() - - if !old_signs && !g:gitgutter_highlight_lines - call gitgutter#all() - endif -endfunction - -function! gitgutter#signs_disable() abort - let g:gitgutter_signs = 0 - call gitgutter#highlight#define_sign_text_highlights() - - if !g:gitgutter_highlight_lines - call gitgutter#sign#clear_signs() - call gitgutter#sign#remove_dummy_sign(0) - endif -endfunction - -function! gitgutter#signs_toggle() abort - if g:gitgutter_signs - call gitgutter#signs_disable() - else - call gitgutter#signs_enable() - endif -endfunction - -" }}} - -" Hunks {{{ - -function! gitgutter#stage_hunk() abort - call gitgutter#utility#use_known_shell() - if gitgutter#utility#is_active() - " Ensure the working copy of the file is up to date. - " It doesn't make sense to stage a hunk otherwise. - noautocmd silent write - let diff = gitgutter#diff#run_diff(0, 1) - call gitgutter#handle_diff(diff) - - if empty(gitgutter#hunk#current_hunk()) - call gitgutter#utility#warn('cursor is not in a hunk') - else - let diff_for_hunk = gitgutter#diff#generate_diff_for_hunk(diff, 'stage') - call gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(g:gitgutter_git_executable.' apply --cached --unidiff-zero - '), diff_for_hunk) - - " refresh gitgutter's view of buffer - silent execute "GitGutter" - endif - - silent! call repeat#set("\GitGutterStageHunk", -1) - endif - call gitgutter#utility#restore_shell() -endfunction - -function! gitgutter#undo_hunk() abort - call gitgutter#utility#use_known_shell() - if gitgutter#utility#is_active() - " Ensure the working copy of the file is up to date. - " It doesn't make sense to stage a hunk otherwise. - noautocmd silent write - let diff = gitgutter#diff#run_diff(0, 1) - call gitgutter#handle_diff(diff) - - if empty(gitgutter#hunk#current_hunk()) - call gitgutter#utility#warn('cursor is not in a hunk') - else - let diff_for_hunk = gitgutter#diff#generate_diff_for_hunk(diff, 'undo') - call gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(g:gitgutter_git_executable.' apply --reverse --unidiff-zero - '), diff_for_hunk) - - " reload file preserving screen line position - " CTRL-Y and CTRL-E treat negative counts as positive counts. - let x = line('w0') - silent edit - let y = line('w0') - let z = x - y - if z > 0 - execute "normal! ".z."\" - else - execute "normal! ".z."\" - endif - endif - - silent! call repeat#set("\GitGutterUndoHunk", -1) - endif - call gitgutter#utility#restore_shell() -endfunction - -function! gitgutter#preview_hunk() abort - call gitgutter#utility#use_known_shell() - if gitgutter#utility#is_active() - " Ensure the working copy of the file is up to date. - " It doesn't make sense to stage a hunk otherwise. - noautocmd silent write - let diff = gitgutter#diff#run_diff(0, 1) - call gitgutter#handle_diff(diff) - - if empty(gitgutter#hunk#current_hunk()) - call gitgutter#utility#warn('cursor is not in a hunk') - else - let diff_for_hunk = gitgutter#diff#generate_diff_for_hunk(diff, 'preview') - - silent! wincmd P - if !&previewwindow - noautocmd execute 'bo' &previewheight 'new' - set previewwindow - endif - - setlocal noro modifiable filetype=diff buftype=nofile bufhidden=delete noswapfile - execute "%delete_" - call append(0, split(diff_for_hunk, "\n")) - - noautocmd wincmd p - endif - endif - call gitgutter#utility#restore_shell() -endfunction - -" }}} diff --git a/autoload/gitgutter/async.vim b/autoload/gitgutter/async.vim index 070e00f..7413930 100644 --- a/autoload/gitgutter/async.vim +++ b/autoload/gitgutter/async.vim @@ -11,10 +11,13 @@ function! gitgutter#async#available() endfunction -function! gitgutter#async#execute(cmd) abort +function! gitgutter#async#execute(cmd, bufnr, handler) abort + call gitgutter#debug#log('[async] '.a:cmd) + let options = { \ 'stdoutbuffer': [], - \ 'buffer': gitgutter#utility#bufnr() + \ 'buffer': a:bufnr, + \ 'handler': a:handler \ } let command = s:build_command(a:cmd) @@ -58,33 +61,13 @@ function! s:on_stdout_nvim(_job_id, data, _event) dict abort endfunction function! s:on_stderr_nvim(_job_id, _data, _event) dict abort - " Backward compatibility for nvim < 0.2.0 - if !has('nvim-0.2.0') - let current_buffer = gitgutter#utility#bufnr() - call gitgutter#utility#set_buffer(self.buffer) - if gitgutter#utility#is_active() - call gitgutter#hunk#reset() - endif - call gitgutter#utility#set_buffer(current_buffer) - return - endif - - call s:buffer_exec(self.buffer, function('gitgutter#hunk#reset')) + call self.handler.err(self.buffer) endfunction -function! s:on_exit_nvim(_job_id, _data, _event) dict abort - " Backward compatibility for nvim < 0.2.0 - if !has('nvim-0.2.0') - let current_buffer = gitgutter#utility#bufnr() - call gitgutter#utility#set_buffer(self.buffer) - if gitgutter#utility#is_active() - call gitgutter#handle_diff(gitgutter#utility#stringify(self.stdoutbuffer)) - endif - call gitgutter#utility#set_buffer(current_buffer) - return +function! s:on_exit_nvim(_job_id, exit_code, _event) dict abort + if !a:exit_code + call self.handler.out(self.buffer, join(self.stdoutbuffer, "\n")) endif - - call s:buffer_exec(self.buffer, function('gitgutter#handle_diff', [gitgutter#utility#stringify(self.stdoutbuffer)])) endfunction @@ -92,22 +75,15 @@ function! s:on_stdout_vim(_channel, data) dict abort call add(self.stdoutbuffer, a:data) endfunction -function! s:on_stderr_vim(_channel, _data) dict abort - call s:buffer_exec(self.buffer, function('gitgutter#hunk#reset')) +function! s:on_stderr_vim(channel, _data) dict abort + call self.handler.err(self.buffer) + try + call ch_close(a:channel) " so close_cb and its 'out' handler are not triggered + catch /E906/ + " noop + endtry endfunction function! s:on_exit_vim(_channel) dict abort - call s:buffer_exec(self.buffer, function('gitgutter#handle_diff', [gitgutter#utility#stringify(self.stdoutbuffer)])) -endfunction - - -function! s:buffer_exec(buffer, fn) - let current_buffer = gitgutter#utility#bufnr() - call gitgutter#utility#set_buffer(a:buffer) - - if gitgutter#utility#is_active() - call a:fn() - endif - - call gitgutter#utility#set_buffer(current_buffer) + call self.handler.out(self.buffer, join(self.stdoutbuffer, "\n")) endfunction diff --git a/autoload/gitgutter/debug.vim b/autoload/gitgutter/debug.vim index 594f044..79d197e 100644 --- a/autoload/gitgutter/debug.vim +++ b/autoload/gitgutter/debug.vim @@ -12,67 +12,67 @@ function! gitgutter#debug#debug() setlocal bufhidden=delete setlocal noswapfile - call gitgutter#debug#vim_version() - call gitgutter#debug#separator() + call s:vim_version() + call s:separator() - call gitgutter#debug#git_version() - call gitgutter#debug#separator() + call s:git_version() + call s:separator() - call gitgutter#debug#grep_version() - call gitgutter#debug#separator() + call s:grep_version() + call s:separator() - call gitgutter#debug#option('updatetime') - call gitgutter#debug#option('shell') - call gitgutter#debug#option('shellcmdflag') - call gitgutter#debug#option('shellpipe') - call gitgutter#debug#option('shellquote') - call gitgutter#debug#option('shellredir') - call gitgutter#debug#option('shellslash') - call gitgutter#debug#option('shelltemp') - call gitgutter#debug#option('shelltype') - call gitgutter#debug#option('shellxescape') - call gitgutter#debug#option('shellxquote') + call s:option('updatetime') + call s:option('shell') + call s:option('shellcmdflag') + call s:option('shellpipe') + call s:option('shellquote') + call s:option('shellredir') + call s:option('shellslash') + call s:option('shelltemp') + call s:option('shelltype') + call s:option('shellxescape') + call s:option('shellxquote') endfunction -function! gitgutter#debug#separator() - call gitgutter#debug#output('') +function! s:separator() + call s:output('') endfunction -function! gitgutter#debug#vim_version() +function! s:vim_version() redir => version_info silent execute 'version' redir END - call gitgutter#debug#output(split(version_info, '\n')[0:2]) + call s:output(split(version_info, '\n')[0:2]) endfunction -function! gitgutter#debug#git_version() +function! s:git_version() let v = system(g:gitgutter_git_executable.' --version') - call gitgutter#debug#output( substitute(v, '\n$', '', '') ) + call s:output( substitute(v, '\n$', '', '') ) endfunction -function! gitgutter#debug#grep_version() +function! s:grep_version() let v = system('grep --version') - call gitgutter#debug#output( substitute(v, '\n$', '', '') ) + call s:output( substitute(v, '\n$', '', '') ) let v = system('grep --help') - call gitgutter#debug#output( substitute(v, '\%x00', '', 'g') ) + call s:output( substitute(v, '\%x00', '', 'g') ) endfunction -function! gitgutter#debug#option(name) +function! s:option(name) if exists('+' . a:name) let v = eval('&' . a:name) - call gitgutter#debug#output(a:name . '=' . v) + call s:output(a:name . '=' . v) " redir => output " silent execute "verbose set " . a:name . "?" " redir END - " call gitgutter#debug#output(a:name . '=' . output) + " call s:output(a:name . '=' . output) else - call gitgutter#debug#output(a:name . ' [n/a]') + call s:output(a:name . ' [n/a]') end endfunction -function! gitgutter#debug#output(text) +function! s:output(text) call append(line('$'), a:text) endfunction diff --git a/autoload/gitgutter/diff.vim b/autoload/gitgutter/diff.vim index 7ecb8af..55fbbcc 100644 --- a/autoload/gitgutter/diff.vim +++ b/autoload/gitgutter/diff.vim @@ -1,152 +1,137 @@ -if exists('g:gitgutter_grep_command') - let s:grep_available = 1 - let s:grep_command = g:gitgutter_grep_command -else - let s:grep_available = executable('grep') - if s:grep_available - let s:grep_command = 'grep' - if $GREP_OPTIONS =~# '--color=always' - let s:grep_command .= ' --color=never' - endif - endif -endif +let s:nomodeline = (v:version > 703 || (v:version == 703 && has('patch442'))) ? '' : '' + let s:hunk_re = '^@@ -\(\d\+\),\?\(\d*\) +\(\d\+\),\?\(\d*\) @@' -let s:c_flag = gitgutter#utility#git_supports_command_line_config_override() +" True for git v1.7.2+. +function! s:git_supports_command_line_config_override() abort + call system(g:gitgutter_git_executable.' -c foo.bar=baz --version') + return !v:shell_error +endfunction + +let s:c_flag = s:git_supports_command_line_config_override() + let s:temp_index = tempname() let s:temp_buffer = tempname() " Returns a diff of the buffer. " -" The way to get the diff depends on whether the buffer is saved or unsaved. +" The buffer contents is not the same as the file on disk so we need to pass +" two instances of the file to git-diff: " -" * Saved: the buffer contents is the same as the file on disk in the working -" tree so we simply do: +" git diff myfileA myfileB " -" git diff myfile +" where myfileA comes from " -" * Unsaved: the buffer contents is not the same as the file on disk so we -" need to pass two instances of the file to git-diff: +" git show :myfile > myfileA " -" git diff myfileA myfileB +" and myfileB is the buffer contents. Ideally we would pass this to +" git-diff on stdin via the second argument to vim's system() function. +" Unfortunately git-diff does not do CRLF conversion for input received on +" stdin, and git-show never performs CRLF conversion, so repos with CRLF +" conversion report that every line is modified due to mismatching EOLs. " -" The first instance is the file in the index which we obtain with: -" -" git show :myfile > myfileA -" -" The second instance is the buffer contents. Ideally we would pass this to -" git-diff on stdin via the second argument to vim's system() function. -" Unfortunately git-diff does not do CRLF conversion for input received on -" stdin, and git-show never performs CRLF conversion, so repos with CRLF -" conversion report that every line is modified due to mismatching EOLs. -" -" Instead, we write the buffer contents to a temporary file - myfileB in this -" example. Note the file extension must be preserved for the CRLF -" conversion to work. -" -" Before diffing a buffer for the first time, we check whether git knows about -" the file: -" -" git ls-files --error-unmatch myfile +" Instead, we write the buffer contents to a temporary file - myfileB in this +" example. Note the file extension must be preserved for the CRLF +" conversion to work. " " After running the diff we pass it through grep where available to reduce " subsequent processing by the plugin. If grep is not available the plugin " does the filtering instead. -function! gitgutter#diff#run_diff(realtime, preserve_full_diff) abort +function! gitgutter#diff#run_diff(bufnr, preserve_full_diff) abort + while gitgutter#utility#repo_path(a:bufnr, 0) == -1 + sleep 5m + endwhile + + if gitgutter#utility#repo_path(a:bufnr, 0) == -2 + throw 'gitgutter not tracked' + endif + + " Wrap compound commands in parentheses to make Windows happy. " bash doesn't mind the parentheses. let cmd = '(' - let bufnr = gitgutter#utility#bufnr() - let tracked = gitgutter#utility#getbufvar(bufnr, 'tracked', 0) " i.e. tracked by git - if !tracked - " Don't bother trying to realtime-diff an untracked file. - " NOTE: perhaps we should pull this guard up to the caller? - if a:realtime - throw 'diff failed' - else - let cmd .= g:gitgutter_git_executable.' ls-files --error-unmatch '.gitgutter#utility#shellescape(gitgutter#utility#filename()).' && (' - endif + let blob_file = s:temp_index + let buff_file = s:temp_buffer + + let extension = gitgutter#utility#extension(a:bufnr) + if !empty(extension) + let blob_file .= '.'.extension + let buff_file .= '.'.extension endif - if a:realtime - let blob_name = g:gitgutter_diff_base.':'.gitgutter#utility#shellescape(gitgutter#utility#file_relative_to_repo_root()) - let blob_file = s:temp_index - let buff_file = s:temp_buffer - let extension = gitgutter#utility#extension() - if !empty(extension) - let blob_file .= '.'.extension - let buff_file .= '.'.extension - endif - let cmd .= g:gitgutter_git_executable.' show '.blob_name.' > '.blob_file.' && ' + " Write file from index to temporary file. + let blob_name = g:gitgutter_diff_base.':'.gitgutter#utility#repo_path(a:bufnr, 1) + let cmd .= g:gitgutter_git_executable.' show '.blob_name.' > '.blob_file.' && ' - " Writing the whole buffer resets the '[ and '] marks and also the - " 'modified' flag (if &cpoptions includes '+'). These are unwanted - " side-effects so we save and restore the values ourselves. - let modified = getbufvar(bufnr, "&mod") - let op_mark_start = getpos("'[") - let op_mark_end = getpos("']") - - let current_buffer = bufnr('') - execute 'buffer '.bufnr - execute 'keepalt noautocmd silent write!' buff_file - execute 'buffer '.current_buffer - - call setbufvar(bufnr, "&mod", modified) - call setpos("'[", op_mark_start) - call setpos("']", op_mark_end) - endif + " Write buffer to temporary file. + call s:write_buffer(a:bufnr, buff_file) + " Call git-diff with the temporary files. let cmd .= g:gitgutter_git_executable if s:c_flag let cmd .= ' -c "diff.autorefreshindex=0"' let cmd .= ' -c "diff.noprefix=false"' endif - let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' ' + let cmd .= ' diff --no-ext-diff --no-color -U0 '.g:gitgutter_diff_args.' -- '.blob_file.' '.buff_file - if a:realtime - let cmd .= ' -- '.blob_file.' '.buff_file - else - let cmd .= g:gitgutter_diff_base.' -- '.gitgutter#utility#shellescape(gitgutter#utility#filename()) + " Pipe git-diff output into grep. + if !a:preserve_full_diff && !empty(g:gitgutter_grep) + let cmd .= ' | '.g:gitgutter_grep.' '.gitgutter#utility#shellescape('^@@ ') endif - if !a:preserve_full_diff && s:grep_available - let cmd .= ' | '.s:grep_command.' '.gitgutter#utility#shellescape('^@@ ') - endif - - if (!a:preserve_full_diff && s:grep_available) || a:realtime - " grep exits with 1 when no matches are found; diff exits with 1 when - " differences are found. However we want to treat non-matches and - " differences as non-erroneous behaviour; so we OR the command with one - " which always exits with success (0). - let cmd .= ' || exit 0' - endif + " grep exits with 1 when no matches are found; git-diff exits with 1 when + " differences are found. However we want to treat non-matches and + " differences as non-erroneous behaviour; so we OR the command with one + " which always exits with success (0). + let cmd .= ' || exit 0' let cmd .= ')' - if !tracked - let cmd .= ')' - endif + let cmd = gitgutter#utility#cd_cmd(a:bufnr, cmd) - let cmd = gitgutter#utility#command_in_directory_of_file(cmd) - - if g:gitgutter_async && gitgutter#async#available() && !a:preserve_full_diff - call gitgutter#async#execute(cmd) + if g:gitgutter_async && gitgutter#async#available() + call gitgutter#async#execute(cmd, a:bufnr, { + \ 'out': function('gitgutter#diff#handler'), + \ 'err': function('gitgutter#hunk#reset'), + \ }) return 'async' else let diff = gitgutter#utility#system(cmd) - if gitgutter#utility#shell_error() - " A shell error indicates the file is not tracked by git (unless something bizarre is going on). - throw 'diff failed' + if v:shell_error + call gitgutter#debug#log(diff) + throw 'gitgutter diff failed' endif return diff endif endfunction + +function! gitgutter#diff#handler(bufnr, diff) abort + call gitgutter#debug#log(a:diff) + + call gitgutter#hunk#set_hunks(a:bufnr, gitgutter#diff#parse_diff(a:diff)) + let modified_lines = s:process_hunks(a:bufnr, gitgutter#hunk#hunks(a:bufnr)) + + if len(modified_lines) > g:gitgutter_max_signs + call gitgutter#utility#warn_once('exceeded maximum number of signs (configured by g:gitgutter_max_signs).', 'max_signs') + call gitgutter#sign#clear_signs(a:bufnr) + + else + if g:gitgutter_signs || g:gitgutter_highlight_lines + call gitgutter#sign#update_signs(a:bufnr, modified_lines) + endif + endif + + call s:save_last_seen_change(a:bufnr) + execute "silent doautocmd" s:nomodeline "User GitGutter" +endfunction + + function! gitgutter#diff#parse_diff(diff) abort let hunks = [] for line in split(a:diff, '\n') @@ -171,69 +156,69 @@ function! gitgutter#diff#parse_hunk(line) abort end endfunction -function! gitgutter#diff#process_hunks(hunks) abort +function! s:process_hunks(bufnr, hunks) abort let modified_lines = [] for hunk in a:hunks - call extend(modified_lines, gitgutter#diff#process_hunk(hunk)) + call extend(modified_lines, s:process_hunk(a:bufnr, hunk)) endfor return modified_lines endfunction " Returns [ [, ], ...] -function! gitgutter#diff#process_hunk(hunk) abort +function! s:process_hunk(bufnr, hunk) abort let modifications = [] let from_line = a:hunk[0] let from_count = a:hunk[1] let to_line = a:hunk[2] let to_count = a:hunk[3] - if gitgutter#diff#is_added(from_count, to_count) - call gitgutter#diff#process_added(modifications, from_count, to_count, to_line) - call gitgutter#hunk#increment_lines_added(to_count) + if s:is_added(from_count, to_count) + call s:process_added(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_added(a:bufnr, to_count) - elseif gitgutter#diff#is_removed(from_count, to_count) - call gitgutter#diff#process_removed(modifications, from_count, to_count, to_line) - call gitgutter#hunk#increment_lines_removed(from_count) + elseif s:is_removed(from_count, to_count) + call s:process_removed(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count) - elseif gitgutter#diff#is_modified(from_count, to_count) - call gitgutter#diff#process_modified(modifications, from_count, to_count, to_line) - call gitgutter#hunk#increment_lines_modified(to_count) + elseif s:is_modified(from_count, to_count) + call s:process_modified(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count) - elseif gitgutter#diff#is_modified_and_added(from_count, to_count) - call gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line) - call gitgutter#hunk#increment_lines_added(to_count - from_count) - call gitgutter#hunk#increment_lines_modified(from_count) + elseif s:is_modified_and_added(from_count, to_count) + call s:process_modified_and_added(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_added(a:bufnr, to_count - from_count) + call gitgutter#hunk#increment_lines_modified(a:bufnr, from_count) - elseif gitgutter#diff#is_modified_and_removed(from_count, to_count) - call gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line) - call gitgutter#hunk#increment_lines_modified(to_count) - call gitgutter#hunk#increment_lines_removed(from_count - to_count) + elseif s:is_modified_and_removed(from_count, to_count) + call s:process_modified_and_removed(modifications, from_count, to_count, to_line) + call gitgutter#hunk#increment_lines_modified(a:bufnr, to_count) + call gitgutter#hunk#increment_lines_removed(a:bufnr, from_count - to_count) endif return modifications endfunction -function! gitgutter#diff#is_added(from_count, to_count) abort +function! s:is_added(from_count, to_count) abort return a:from_count == 0 && a:to_count > 0 endfunction -function! gitgutter#diff#is_removed(from_count, to_count) abort +function! s:is_removed(from_count, to_count) abort return a:from_count > 0 && a:to_count == 0 endfunction -function! gitgutter#diff#is_modified(from_count, to_count) abort +function! s:is_modified(from_count, to_count) abort return a:from_count > 0 && a:to_count > 0 && a:from_count == a:to_count endfunction -function! gitgutter#diff#is_modified_and_added(from_count, to_count) abort +function! s:is_modified_and_added(from_count, to_count) abort return a:from_count > 0 && a:to_count > 0 && a:from_count < a:to_count endfunction -function! gitgutter#diff#is_modified_and_removed(from_count, to_count) abort +function! s:is_modified_and_removed(from_count, to_count) abort return a:from_count > 0 && a:to_count > 0 && a:from_count > a:to_count endfunction -function! gitgutter#diff#process_added(modifications, from_count, to_count, to_line) abort +function! s:process_added(modifications, from_count, to_count, to_line) abort let offset = 0 while offset < a:to_count let line_number = a:to_line + offset @@ -242,7 +227,7 @@ function! gitgutter#diff#process_added(modifications, from_count, to_count, to_l endwhile endfunction -function! gitgutter#diff#process_removed(modifications, from_count, to_count, to_line) abort +function! s:process_removed(modifications, from_count, to_count, to_line) abort if a:to_line == 0 call add(a:modifications, [1, 'removed_first_line']) else @@ -250,7 +235,7 @@ function! gitgutter#diff#process_removed(modifications, from_count, to_count, to endif endfunction -function! gitgutter#diff#process_modified(modifications, from_count, to_count, to_line) abort +function! s:process_modified(modifications, from_count, to_count, to_line) abort let offset = 0 while offset < a:to_count let line_number = a:to_line + offset @@ -259,7 +244,7 @@ function! gitgutter#diff#process_modified(modifications, from_count, to_count, t endwhile endfunction -function! gitgutter#diff#process_modified_and_added(modifications, from_count, to_count, to_line) abort +function! s:process_modified_and_added(modifications, from_count, to_count, to_line) abort let offset = 0 while offset < a:from_count let line_number = a:to_line + offset @@ -273,7 +258,7 @@ function! gitgutter#diff#process_modified_and_added(modifications, from_count, t endwhile endfunction -function! gitgutter#diff#process_modified_and_removed(modifications, from_count, to_count, to_line) abort +function! s:process_modified_and_removed(modifications, from_count, to_count, to_line) abort let offset = 0 while offset < a:to_count let line_number = a:to_line + offset @@ -283,28 +268,14 @@ function! gitgutter#diff#process_modified_and_removed(modifications, from_count, let a:modifications[-1] = [a:to_line + offset - 1, 'modified_removed'] endfunction -" Generates a zero-context diff for the current hunk. -" -" diff - the full diff for the buffer -" type - stage | undo | preview -function! gitgutter#diff#generate_diff_for_hunk(diff, type) abort - let diff_for_hunk = gitgutter#diff#discard_hunks(a:diff, a:type == 'stage' || a:type == 'undo') - if a:type == 'stage' || a:type == 'undo' - let diff_for_hunk = gitgutter#diff#adjust_hunk_summary(diff_for_hunk, a:type == 'stage') - endif - - return diff_for_hunk -endfunction - -" Returns the diff with all hunks discarded except the current. -" -" diff - the diff to process -" keep_header - truthy to keep the diff header and hunk summary, falsy to discard it -function! gitgutter#diff#discard_hunks(diff, keep_header) abort +" Returns a diff for the current hunk. +function! gitgutter#diff#hunk_diff(bufnr, full_diff) let modified_diff = [] - let keep_line = a:keep_header - for line in split(a:diff, '\n') + let keep_line = 1 + " Don't keepempty when splitting because the diff we want may not be the + " final one. Instead add trailing NL at end of function. + for line in split(a:full_diff, '\n') let hunk_info = gitgutter#diff#parse_hunk(line) if len(hunk_info) == 4 " start of new hunk let keep_line = gitgutter#hunk#cursor_in_hunk(hunk_info) @@ -313,36 +284,36 @@ function! gitgutter#diff#discard_hunks(diff, keep_header) abort call add(modified_diff, line) endif endfor - - if a:keep_header - return gitgutter#utility#stringify(modified_diff) - else - " Discard hunk summary too. - return gitgutter#utility#stringify(modified_diff[1:]) - endif + return join(modified_diff, "\n")."\n" endfunction -" Adjust hunk summary (from's / to's line number) to ignore changes above/before this one. -" -" diff_for_hunk - a diff containing only the hunk of interest -" staging - truthy if the hunk is to be staged, falsy if it is to be undone -" -" TODO: push this down to #discard_hunks? -function! gitgutter#diff#adjust_hunk_summary(diff_for_hunk, staging) abort - let line_adjustment = gitgutter#hunk#line_adjustment_for_current_hunk() - let adj_diff = [] - for line in split(a:diff_for_hunk, '\n') - if match(line, s:hunk_re) != -1 - if a:staging - " increment 'to' line number - let line = substitute(line, '+\@<=\(\d\+\)', '\=submatch(1)+line_adjustment', '') - else - " decrement 'from' line number - let line = substitute(line, '-\@<=\(\d\+\)', '\=submatch(1)-line_adjustment', '') - endif - endif - call add(adj_diff, line) - endfor - return gitgutter#utility#stringify(adj_diff) + +function! s:write_buffer(bufnr, file) + " Write specified buffer (which may not be the current buffer) to buff_file. + " There doesn't seem to be a clean way to write a buffer that isn't the current + " to a file; we have to switch to it, write it, then switch back. + let current_buffer = bufnr('') + execute 'buffer '.a:bufnr + + " Writing the whole buffer resets the '[ and '] marks and also the + " 'modified' flag (if &cpoptions includes '+'). These are unwanted + " side-effects so we save and restore the values ourselves. + let modified = getbufvar(a:bufnr, "&mod") + let op_mark_start = getpos("'[") + let op_mark_end = getpos("']") + + execute 'keepalt noautocmd silent write!' a:file + + call setbufvar(a:bufnr, "&mod", modified) + call setpos("'[", op_mark_start) + call setpos("']", op_mark_end) + + execute 'buffer '.current_buffer endfunction + +function! s:save_last_seen_change(bufnr) abort + call gitgutter#utility#setbufvar(a:bufnr, 'tick', getbufvar(a:bufnr, 'changedtick')) +endfunction + + diff --git a/autoload/gitgutter/highlight.vim b/autoload/gitgutter/highlight.vim index 61b0f30..3fa4a91 100644 --- a/autoload/gitgutter/highlight.vim +++ b/autoload/gitgutter/highlight.vim @@ -1,3 +1,37 @@ +function! gitgutter#highlight#line_disable() abort + let g:gitgutter_highlight_lines = 0 + call s:define_sign_line_highlights() + + if !g:gitgutter_signs + call gitgutter#sign#clear_signs(bufnr('')) + call gitgutter#sign#remove_dummy_sign(bufnr(''), 0) + endif + + redraw! +endfunction + +function! gitgutter#highlight#line_enable() abort + let old_highlight_lines = g:gitgutter_highlight_lines + + let g:gitgutter_highlight_lines = 1 + call s:define_sign_line_highlights() + + if !old_highlight_lines && !g:gitgutter_signs + call gitgutter#all(1) + endif + + redraw! +endfunction + +function! gitgutter#highlight#line_toggle() abort + if g:gitgutter_highlight_lines + call gitgutter#highlight#line_disable() + else + call gitgutter#highlight#line_enable() + endif +endfunction + + function! gitgutter#highlight#define_sign_column_highlight() abort if g:gitgutter_override_sign_column_highlight highlight! link SignColumn LineNr @@ -7,7 +41,7 @@ function! gitgutter#highlight#define_sign_column_highlight() abort endfunction function! gitgutter#highlight#define_highlights() abort - let [guibg, ctermbg] = gitgutter#highlight#get_background_colors('SignColumn') + let [guibg, ctermbg] = s:get_background_colors('SignColumn') " Highlights used by the signs. @@ -42,12 +76,12 @@ function! gitgutter#highlight#define_signs() abort sign define GitGutterLineModifiedRemoved sign define GitGutterDummy - call gitgutter#highlight#define_sign_text() + call s:define_sign_text() call gitgutter#highlight#define_sign_text_highlights() - call gitgutter#highlight#define_sign_line_highlights() + call s:define_sign_line_highlights() endfunction -function! gitgutter#highlight#define_sign_text() abort +function! s:define_sign_text() abort execute "sign define GitGutterLineAdded text=" . g:gitgutter_sign_added execute "sign define GitGutterLineModified text=" . g:gitgutter_sign_modified execute "sign define GitGutterLineRemoved text=" . g:gitgutter_sign_removed @@ -75,7 +109,7 @@ function! gitgutter#highlight#define_sign_text_highlights() abort endif endfunction -function! gitgutter#highlight#define_sign_line_highlights() abort +function! s:define_sign_line_highlights() abort if g:gitgutter_highlight_lines sign define GitGutterLineAdded linehl=GitGutterAddLine sign define GitGutterLineModified linehl=GitGutterChangeLine @@ -91,22 +125,22 @@ function! gitgutter#highlight#define_sign_line_highlights() abort endif endfunction -function! gitgutter#highlight#get_background_colors(group) abort +function! s:get_background_colors(group) abort redir => highlight silent execute 'silent highlight ' . a:group redir END let link_matches = matchlist(highlight, 'links to \(\S\+\)') if len(link_matches) > 0 " follow the link - return gitgutter#highlight#get_background_colors(link_matches[1]) + return s:get_background_colors(link_matches[1]) endif - let ctermbg = gitgutter#highlight#match_highlight(highlight, 'ctermbg=\([0-9A-Za-z]\+\)') - let guibg = gitgutter#highlight#match_highlight(highlight, 'guibg=\([#0-9A-Za-z]\+\)') + let ctermbg = s:match_highlight(highlight, 'ctermbg=\([0-9A-Za-z]\+\)') + let guibg = s:match_highlight(highlight, 'guibg=\([#0-9A-Za-z]\+\)') return [guibg, ctermbg] endfunction -function! gitgutter#highlight#match_highlight(highlight, pattern) abort +function! s:match_highlight(highlight, pattern) abort let matches = matchlist(a:highlight, a:pattern) if len(matches) == 0 return 'NONE' diff --git a/autoload/gitgutter/hunk.vim b/autoload/gitgutter/hunk.vim index 5875e0a..fd8de80 100644 --- a/autoload/gitgutter/hunk.vim +++ b/autoload/gitgutter/hunk.vim @@ -1,15 +1,15 @@ -function! gitgutter#hunk#set_hunks(hunks) abort - call gitgutter#utility#setbufvar(gitgutter#utility#bufnr(), 'hunks', a:hunks) - call s:reset_summary() +function! gitgutter#hunk#set_hunks(bufnr, hunks) abort + call gitgutter#utility#setbufvar(a:bufnr, 'hunks', a:hunks) + call s:reset_summary(a:bufnr) endfunction -function! gitgutter#hunk#hunks() abort - return gitgutter#utility#getbufvar(gitgutter#utility#bufnr(), 'hunks', []) +function! gitgutter#hunk#hunks(bufnr) abort + return gitgutter#utility#getbufvar(a:bufnr, 'hunks', []) endfunction -function! gitgutter#hunk#reset() abort - call gitgutter#utility#setbufvar(gitgutter#utility#bufnr(), 'hunks', []) - call s:reset_summary() +function! gitgutter#hunk#reset(bufnr) abort + call gitgutter#utility#setbufvar(a:bufnr, 'hunks', []) + call s:reset_summary(a:bufnr) endfunction @@ -17,37 +17,35 @@ function! gitgutter#hunk#summary(bufnr) abort return gitgutter#utility#getbufvar(a:bufnr, 'summary', [0,0,0]) endfunction -function! s:reset_summary() abort - call gitgutter#utility#setbufvar(gitgutter#utility#bufnr(), 'summary', [0,0,0]) +function! s:reset_summary(bufnr) abort + call gitgutter#utility#setbufvar(a:bufnr, 'summary', [0,0,0]) endfunction -function! gitgutter#hunk#increment_lines_added(count) abort - let bufnr = gitgutter#utility#bufnr() - let summary = gitgutter#hunk#summary(bufnr) +function! gitgutter#hunk#increment_lines_added(bufnr, count) abort + let summary = gitgutter#hunk#summary(a:bufnr) let summary[0] += a:count - call gitgutter#utility#setbufvar(bufnr, 'summary', summary) + call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary) endfunction -function! gitgutter#hunk#increment_lines_modified(count) abort - let bufnr = gitgutter#utility#bufnr() - let summary = gitgutter#hunk#summary(bufnr) +function! gitgutter#hunk#increment_lines_modified(bufnr, count) abort + let summary = gitgutter#hunk#summary(a:bufnr) let summary[1] += a:count - call gitgutter#utility#setbufvar(bufnr, 'summary', summary) + call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary) endfunction -function! gitgutter#hunk#increment_lines_removed(count) abort - let bufnr = gitgutter#utility#bufnr() - let summary = gitgutter#hunk#summary(bufnr) +function! gitgutter#hunk#increment_lines_removed(bufnr, count) abort + let summary = gitgutter#hunk#summary(a:bufnr) let summary[2] += a:count - call gitgutter#utility#setbufvar(bufnr, 'summary', summary) + call gitgutter#utility#setbufvar(a:bufnr, 'summary', summary) endfunction function! gitgutter#hunk#next_hunk(count) abort - if gitgutter#utility#is_active() + let bufnr = bufnr('') + if gitgutter#utility#is_active(bufnr) let current_line = line('.') let hunk_count = 0 - for hunk in gitgutter#hunk#hunks() + for hunk in gitgutter#hunk#hunks(bufnr) if hunk[2] > current_line let hunk_count += 1 if hunk_count == a:count @@ -61,10 +59,11 @@ function! gitgutter#hunk#next_hunk(count) abort endfunction function! gitgutter#hunk#prev_hunk(count) abort - if gitgutter#utility#is_active() + let bufnr = bufnr('') + if gitgutter#utility#is_active(bufnr) let current_line = line('.') let hunk_count = 0 - for hunk in reverse(copy(gitgutter#hunk#hunks())) + for hunk in reverse(copy(gitgutter#hunk#hunks(bufnr))) if hunk[2] < current_line let hunk_count += 1 if hunk_count == a:count @@ -80,10 +79,11 @@ endfunction " Returns the hunk the cursor is currently in or an empty list if the cursor " isn't in a hunk. -function! gitgutter#hunk#current_hunk() abort +function! s:current_hunk() abort + let bufnr = bufnr('') let current_hunk = [] - for hunk in gitgutter#hunk#hunks() + for hunk in gitgutter#hunk#hunks(bufnr) if gitgutter#hunk#cursor_in_hunk(hunk) let current_hunk = hunk break @@ -107,22 +107,9 @@ function! gitgutter#hunk#cursor_in_hunk(hunk) abort return 0 endfunction -" Returns the number of lines the current hunk is offset from where it would -" be if any changes above it in the file didn't exist. -function! gitgutter#hunk#line_adjustment_for_current_hunk() abort - let adj = 0 - for hunk in gitgutter#hunk#hunks() - if gitgutter#hunk#cursor_in_hunk(hunk) - break - else - let adj += hunk[1] - hunk[3] - endif - endfor - return adj -endfunction - function! gitgutter#hunk#text_object(inner) abort - let hunk = gitgutter#hunk#current_hunk() + let bufnr = bufnr('') + let hunk = s:current_hunk(bufnr) if empty(hunk) return @@ -141,3 +128,129 @@ function! gitgutter#hunk#text_object(inner) abort execute 'normal! 'first_line.'GV'.last_line.'G' endfunction + + +function! gitgutter#hunk#stage() abort + call s:hunk_op(function('s:stage')) +endfunction + +function! gitgutter#hunk#undo() abort + call s:hunk_op(function('s:undo')) +endfunction + +function! gitgutter#hunk#preview() abort + call s:hunk_op(function('s:preview')) +endfunction + + +function! s:hunk_op(op) + let bufnr = bufnr('') + + if gitgutter#utility#is_active(bufnr) + " Get a (synchronous) diff. + let [async, g:gitgutter_async] = [g:gitgutter_async, 0] + let diff = gitgutter#diff#run_diff(bufnr, 1) + let g:gitgutter_async = async + + call gitgutter#hunk#set_hunks(bufnr, gitgutter#diff#parse_diff(diff)) + + if empty(s:current_hunk()) + call gitgutter#utility#warn('cursor is not in a hunk') + else + call a:op(gitgutter#diff#hunk_diff(bufnr, diff)) + endif + endif +endfunction + + +function! s:stage(hunk_diff) + let bufnr = bufnr('') + let diff = s:adjust_header(bufnr, a:hunk_diff) + " Apply patch to index. + call gitgutter#utility#system( + \ gitgutter#utility#cd_cmd(bufnr, g:gitgutter_git_executable.' apply --cached --unidiff-zero - '), + \ diff) + + " Refresh gitgutter's view of buffer. + call gitgutter#process_buffer(bufnr, 1) +endfunction + + +function! s:undo(hunk_diff) + " Apply reverse patch to buffer. + let hunk = gitgutter#diff#parse_hunk(split(a:hunk_diff, '\n')[4]) + let lines = map(split(a:hunk_diff, '\n')[5:], 'v:val[1:]') + let lnum = hunk[2] + let added_only = hunk[1] == 0 && hunk[3] > 0 + let removed_only = hunk[1] > 0 && hunk[3] == 0 + + if removed_only + call append(lnum, lines) + elseif added_only + execute lnum .','. (lnum+len(lines)-1) .'d' + else + call append(lnum-1, lines[0:hunk[1]]) + execute (lnum+hunk[1]) .','. (lnum+hunk[1]+hunk[3]) .'d' + endif +endfunction + + +function! s:preview(hunk_diff) + silent! wincmd P + if !&previewwindow + noautocmd execute 'bo' &previewheight 'new' + set previewwindow + endif + + setlocal noro modifiable filetype=diff buftype=nofile bufhidden=delete noswapfile + execute "%delete_" + call append(0, split(s:discard_header(a:hunk_diff), "\n")) + + noautocmd wincmd p +endfunction + + +function! s:adjust_header(bufnr, hunk_diff) + return s:adjust_hunk_summary(s:fix_file_references(a:bufnr, a:hunk_diff)) +endfunction + + +" Replaces references to temp files with the actual file. +function! s:fix_file_references(bufnr, hunk_diff) + let filepath = gitgutter#utility#repo_path(a:bufnr, 0) + let diff = a:hunk_diff + for tmp in matchlist(diff, '\vdiff --git a/(\S+) b/(\S+)\n')[1:2] + let diff = substitute(diff, tmp, filepath, 'g') + endfor + return diff +endfunction + + +function! s:adjust_hunk_summary(hunk_diff) abort + let line_adjustment = s:line_adjustment_for_current_hunk() + let diff = split(a:hunk_diff, '\n', 1) + let diff[4] = substitute(diff[4], '+\@<=\(\d\+\)', '\=submatch(1)+line_adjustment', '') + return join(diff, "\n") +endfunction + + +function! s:discard_header(hunk_diff) + return join(split(a:hunk_diff, '\n', 1)[5:], "\n") +endfunction + + +" Returns the number of lines the current hunk is offset from where it would +" be if any changes above it in the file didn't exist. +function! s:line_adjustment_for_current_hunk() abort + let bufnr = bufnr('') + let adj = 0 + for hunk in gitgutter#hunk#hunks(bufnr) + if gitgutter#hunk#cursor_in_hunk(hunk) + break + else + let adj += hunk[1] - hunk[3] + endif + endfor + return adj +endfunction + diff --git a/autoload/gitgutter/sign.vim b/autoload/gitgutter/sign.vim index cab3d65..b65563d 100644 --- a/autoload/gitgutter/sign.vim +++ b/autoload/gitgutter/sign.vim @@ -10,14 +10,43 @@ let s:dummy_sign_id = s:first_sign_id - 1 let s:supports_star = v:version > 703 || (v:version == 703 && has("patch596")) -" Removes gitgutter's signs (excluding dummy sign) from the buffer being processed. -function! gitgutter#sign#clear_signs() abort - let bufnr = gitgutter#utility#bufnr() - call gitgutter#sign#find_current_signs() +function! gitgutter#sign#enable() abort + let old_signs = g:gitgutter_signs - let sign_ids = map(values(gitgutter#utility#getbufvar(bufnr, 'gitgutter_signs')), 'v:val.id') - call gitgutter#sign#remove_signs(sign_ids, 1) - call gitgutter#utility#setbufvar(bufnr, 'gitgutter_signs', {}) + let g:gitgutter_signs = 1 + call gitgutter#highlight#define_sign_text_highlights() + + if !old_signs && !g:gitgutter_highlight_lines + call gitgutter#all(1) + endif +endfunction + +function! gitgutter#sign#disable() abort + let g:gitgutter_signs = 0 + call gitgutter#highlight#define_sign_text_highlights() + + if !g:gitgutter_highlight_lines + call gitgutter#sign#clear_signs(bufnr('')) + call gitgutter#sign#remove_dummy_sign(bufnr(''), 0) + endif +endfunction + +function! gitgutter#sign#toggle() abort + if g:gitgutter_signs + call gitgutter#sign#disable() + else + call gitgutter#sign#enable() + endif +endfunction + + +" Removes gitgutter's signs (excluding dummy sign) from the buffer being processed. +function! gitgutter#sign#clear_signs(bufnr) abort + call s:find_current_signs(a:bufnr) + + let sign_ids = map(values(gitgutter#utility#getbufvar(a:bufnr, 'gitgutter_signs')), 'v:val.id') + call s:remove_signs(a:bufnr, sign_ids, 1) + call gitgutter#utility#setbufvar(a:bufnr, 'gitgutter_signs', {}) endfunction @@ -25,39 +54,37 @@ endfunction " " modified_lines: list of [, ] " where name = 'added|removed|modified|modified_removed' -function! gitgutter#sign#update_signs(modified_lines) abort - call gitgutter#sign#find_current_signs() +function! gitgutter#sign#update_signs(bufnr, modified_lines) abort + call s:find_current_signs(a:bufnr) let new_gitgutter_signs_line_numbers = map(copy(a:modified_lines), 'v:val[0]') - let obsolete_signs = gitgutter#sign#obsolete_gitgutter_signs_to_remove(new_gitgutter_signs_line_numbers) + let obsolete_signs = s:obsolete_gitgutter_signs_to_remove(a:bufnr, new_gitgutter_signs_line_numbers) let flicker_possible = s:remove_all_old_signs && !empty(a:modified_lines) if flicker_possible - call gitgutter#sign#add_dummy_sign() + call s:add_dummy_sign(a:bufnr) endif - call gitgutter#sign#remove_signs(obsolete_signs, s:remove_all_old_signs) - call gitgutter#sign#upsert_new_gitgutter_signs(a:modified_lines) + call s:remove_signs(a:bufnr, obsolete_signs, s:remove_all_old_signs) + call s:upsert_new_gitgutter_signs(a:bufnr, a:modified_lines) if flicker_possible - call gitgutter#sign#remove_dummy_sign(0) + call gitgutter#sign#remove_dummy_sign(a:bufnr, 0) endif endfunction -function! gitgutter#sign#add_dummy_sign() abort - let bufnr = gitgutter#utility#bufnr() - if !gitgutter#utility#getbufvar(bufnr, 'dummy_sign') - execute "sign place" s:dummy_sign_id "line=" . 9999 "name=GitGutterDummy buffer=" . bufnr - call gitgutter#utility#setbufvar(bufnr, 'dummy_sign', 1) +function! s:add_dummy_sign(bufnr) abort + if !gitgutter#utility#getbufvar(a:bufnr, 'dummy_sign') + execute "sign place" s:dummy_sign_id "line=" . 9999 "name=GitGutterDummy buffer=" . a:bufnr + call gitgutter#utility#setbufvar(a:bufnr, 'dummy_sign', 1) endif endfunction -function! gitgutter#sign#remove_dummy_sign(force) abort - let bufnr = gitgutter#utility#bufnr() - if gitgutter#utility#getbufvar(bufnr, 'dummy_sign') && (a:force || !g:gitgutter_sign_column_always) - execute "sign unplace" s:dummy_sign_id "buffer=" . bufnr - call gitgutter#utility#setbufvar(bufnr, 'dummy_sign', 0) +function! gitgutter#sign#remove_dummy_sign(bufnr, force) abort + if gitgutter#utility#getbufvar(a:bufnr, 'dummy_sign') && (a:force || !g:gitgutter_sign_column_always) + execute "sign unplace" s:dummy_sign_id "buffer=" . a:bufnr + call gitgutter#utility#setbufvar(a:bufnr, 'dummy_sign', 0) endif endfunction @@ -67,14 +94,13 @@ endfunction " -function! gitgutter#sign#find_current_signs() abort - let bufnr = gitgutter#utility#bufnr() +function! s:find_current_signs(bufnr) abort let gitgutter_signs = {} " : {'id': , 'name': } let other_signs = [] " [ signs - silent execute "sign place buffer=" . bufnr + silent execute "sign place buffer=" . a:bufnr redir END for sign_line in filter(split(signs, '\n')[2:], 'v:val =~# "="') @@ -101,19 +127,18 @@ function! gitgutter#sign#find_current_signs() abort end endfor - call gitgutter#utility#setbufvar(bufnr, 'dummy_sign', dummy_sign_placed) - call gitgutter#utility#setbufvar(bufnr, 'gitgutter_signs', gitgutter_signs) - call gitgutter#utility#setbufvar(bufnr, 'other_signs', other_signs) + call gitgutter#utility#setbufvar(a:bufnr, 'dummy_sign', dummy_sign_placed) + call gitgutter#utility#setbufvar(a:bufnr, 'gitgutter_signs', gitgutter_signs) + call gitgutter#utility#setbufvar(a:bufnr, 'other_signs', other_signs) endfunction " Returns a list of [, ...] " Sets `s:remove_all_old_signs` as a side-effect. -function! gitgutter#sign#obsolete_gitgutter_signs_to_remove(new_gitgutter_signs_line_numbers) abort - let bufnr = gitgutter#utility#bufnr() +function! s:obsolete_gitgutter_signs_to_remove(bufnr, new_gitgutter_signs_line_numbers) abort let signs_to_remove = [] " list of [, ...] let remove_all_signs = 1 - let old_gitgutter_signs = gitgutter#utility#getbufvar(bufnr, 'gitgutter_signs') + let old_gitgutter_signs = gitgutter#utility#getbufvar(a:bufnr, 'gitgutter_signs') for line_number in keys(old_gitgutter_signs) if index(a:new_gitgutter_signs_line_numbers, str2nr(line_number)) == -1 call add(signs_to_remove, old_gitgutter_signs[line_number].id) @@ -126,13 +151,12 @@ function! gitgutter#sign#obsolete_gitgutter_signs_to_remove(new_gitgutter_signs_ endfunction -function! gitgutter#sign#remove_signs(sign_ids, all_signs) abort - let bufnr = gitgutter#utility#bufnr() - if a:all_signs && s:supports_star && empty(gitgutter#utility#getbufvar(bufnr, 'other_signs')) - let dummy_sign_present = gitgutter#utility#getbufvar(bufnr, 'dummy_sign') - execute "sign unplace * buffer=" . bufnr +function! s:remove_signs(bufnr, sign_ids, all_signs) abort + if a:all_signs && s:supports_star && empty(gitgutter#utility#getbufvar(a:bufnr, 'other_signs')) + let dummy_sign_present = gitgutter#utility#getbufvar(a:bufnr, 'dummy_sign') + execute "sign unplace * buffer=" . a:bufnr if dummy_sign_present - execute "sign place" s:dummy_sign_id "line=" . 9999 "name=GitGutterDummy buffer=" . bufnr + execute "sign place" s:dummy_sign_id "line=" . 9999 "name=GitGutterDummy buffer=" . a:bufnr endif else for id in a:sign_ids @@ -142,22 +166,21 @@ function! gitgutter#sign#remove_signs(sign_ids, all_signs) abort endfunction -function! gitgutter#sign#upsert_new_gitgutter_signs(modified_lines) abort - let bufnr = gitgutter#utility#bufnr() - let other_signs = gitgutter#utility#getbufvar(bufnr, 'other_signs') - let old_gitgutter_signs = gitgutter#utility#getbufvar(bufnr, 'gitgutter_signs') +function! s:upsert_new_gitgutter_signs(bufnr, modified_lines) abort + let other_signs = gitgutter#utility#getbufvar(a:bufnr, 'other_signs') + let old_gitgutter_signs = gitgutter#utility#getbufvar(a:bufnr, 'gitgutter_signs') for line in a:modified_lines let line_number = line[0] " if index(other_signs, line_number) == -1 " don't clobber others' signs - let name = gitgutter#utility#highlight_name_for_change(line[1]) + let name = s:highlight_name_for_change(line[1]) if !has_key(old_gitgutter_signs, line_number) " insert - let id = gitgutter#sign#next_sign_id() - execute "sign place" id "line=" . line_number "name=" . name "buffer=" . bufnr + let id = s:next_sign_id() + execute "sign place" id "line=" . line_number "name=" . name "buffer=" . a:bufnr else " update if sign has changed let old_sign = old_gitgutter_signs[line_number] if old_sign.name !=# name - execute "sign place" old_sign.id "name=" . name "buffer=" . bufnr + execute "sign place" old_sign.id "name=" . name "buffer=" . a:bufnr end endif endif @@ -166,7 +189,7 @@ function! gitgutter#sign#upsert_new_gitgutter_signs(modified_lines) abort endfunction -function! gitgutter#sign#next_sign_id() abort +function! s:next_sign_id() abort let next_id = s:next_sign_id let s:next_sign_id += 1 return next_id @@ -177,3 +200,20 @@ endfunction function! gitgutter#sign#reset() let s:next_sign_id = s:first_sign_id endfunction + + +function! s:highlight_name_for_change(text) abort + if a:text ==# 'added' + return 'GitGutterLineAdded' + elseif a:text ==# 'removed' + return 'GitGutterLineRemoved' + elseif a:text ==# 'removed_first_line' + return 'GitGutterLineRemovedFirstLine' + elseif a:text ==# 'modified' + return 'GitGutterLineModified' + elseif a:text ==# 'modified_removed' + return 'GitGutterLineModifiedRemoved' + endif +endfunction + + diff --git a/autoload/gitgutter/utility.vim b/autoload/gitgutter/utility.vim index e4bc774..abb6c37 100644 --- a/autoload/gitgutter/utility.vim +++ b/autoload/gitgutter/utility.vim @@ -1,7 +1,3 @@ -let s:file = '' -let s:using_xolox_shell = -1 -let s:exit_code = 0 - function! gitgutter#utility#setbufvar(buffer, varname, val) let dict = get(getbufvar(a:buffer, ''), 'gitgutter', {}) let dict[a:varname] = a:val @@ -38,188 +34,128 @@ endfunction " Returns truthy when the buffer's file should be processed; and falsey when it shouldn't. " This function does not and should not make any system calls. -function! gitgutter#utility#is_active() abort +function! gitgutter#utility#is_active(bufnr) abort return g:gitgutter_enabled && \ !pumvisible() && - \ gitgutter#utility#is_file_buffer() && - \ gitgutter#utility#exists_file() && - \ gitgutter#utility#not_git_dir() + \ s:is_file_buffer(a:bufnr) && + \ s:exists_file(a:bufnr) && + \ s:not_git_dir(a:bufnr) endfunction -function! gitgutter#utility#not_git_dir() abort - return gitgutter#utility#full_path_to_directory_of_file() !~ '[/\\]\.git\($\|[/\\]\)' +function! s:not_git_dir(bufnr) abort + return s:dir(a:bufnr) !~ '[/\\]\.git\($\|[/\\]\)' endfunction -function! gitgutter#utility#is_file_buffer() abort - return empty(getbufvar(s:bufnr, '&buftype')) +function! s:is_file_buffer(bufnr) abort + return empty(getbufvar(a:bufnr, '&buftype')) endfunction -" A replacement for the built-in `shellescape(arg)`. -" -" Recent versions of Vim handle shell escaping pretty well. However older -" versions aren't as good. This attempts to do the right thing. -" -" See: -" https://github.com/tpope/vim-fugitive/blob/8f0b8edfbd246c0026b7a2388e1d883d579ac7f6/plugin/fugitive.vim#L29-L37 +" From tpope/vim-fugitive +function! s:winshell() + return &shell =~? 'cmd' || exists('+shellslash') && !&shellslash +endfunction + +" From tpope/vim-fugitive function! gitgutter#utility#shellescape(arg) abort if a:arg =~ '^[A-Za-z0-9_/.-]\+$' return a:arg - elseif &shell =~# 'cmd' || gitgutter#utility#using_xolox_shell() + elseif s:winshell() return '"' . substitute(substitute(a:arg, '"', '""', 'g'), '%', '"%"', 'g') . '"' else return shellescape(a:arg) endif endfunction -function! gitgutter#utility#set_buffer(bufnr) abort - let s:bufnr = a:bufnr - let s:file = resolve(bufname(a:bufnr)) +function! gitgutter#utility#file(bufnr) + return s:abs_path(a:bufnr, 1) endfunction -function! gitgutter#utility#bufnr() - return s:bufnr -endfunction - -function! gitgutter#utility#file() - return s:file -endfunction - -function! gitgutter#utility#filename() abort - return fnamemodify(s:file, ':t') -endfunction - -function! gitgutter#utility#extension() abort - return fnamemodify(s:file, ':e') -endfunction - -function! gitgutter#utility#full_path_to_directory_of_file() abort - return fnamemodify(s:file, ':p:h') -endfunction - -function! gitgutter#utility#directory_of_file() abort - return fnamemodify(s:file, ':h') -endfunction - -function! gitgutter#utility#exists_file() abort - return filereadable(s:file) -endfunction - -function! gitgutter#utility#has_unsaved_changes() abort - return getbufvar(s:bufnr, "&mod") -endfunction - -function! gitgutter#utility#has_fresh_changes() abort - return getbufvar(s:bufnr, 'changedtick') != gitgutter#utility#getbufvar(s:bufnr, 'last_tick') -endfunction - -function! gitgutter#utility#save_last_seen_change() abort - call gitgutter#utility#setbufvar(s:bufnr, 'last_tick', getbufvar(s:bufnr, 'changedtick')) -endfunction - -function! gitgutter#utility#shell_error() abort - return gitgutter#utility#using_xolox_shell() ? s:exit_code : v:shell_error -endfunction - -function! gitgutter#utility#using_xolox_shell() abort - if s:using_xolox_shell == -1 - if !g:gitgutter_avoid_cmd_prompt_on_windows - let s:using_xolox_shell = 0 - " Although xolox/vim-shell works on both windows and unix we only want to use - " it on windows. - elseif has('win32') || has('win64') || has('win32unix') - let s:using_xolox_shell = exists('g:xolox#misc#version') && exists('g:xolox#shell#version') - else - let s:using_xolox_shell = 0 - endif - endif - return s:using_xolox_shell +" Not shellescaped +function! gitgutter#utility#extension(bufnr) abort + return fnamemodify(s:abs_path(a:bufnr, 0), ':e') endfunction function! gitgutter#utility#system(cmd, ...) abort call gitgutter#debug#log(a:cmd, a:000) - if gitgutter#utility#using_xolox_shell() - let options = {'command': a:cmd, 'check': 0} - if a:0 > 0 - let options['stdin'] = a:1 - endif - let ret = xolox#misc#os#exec(options) - let output = join(ret.stdout, "\n") - let s:exit_code = ret.exit_code - else - silent let output = (a:0 == 0) ? system(a:cmd) : system(a:cmd, a:1) - endif + call s:use_known_shell() + silent let output = (a:0 == 0) ? system(a:cmd) : system(a:cmd, a:1) + call s:restore_shell() + return output endfunction -function! gitgutter#utility#file_relative_to_repo_root() abort - let file_path_relative_to_repo_root = gitgutter#utility#getbufvar(s:bufnr, 'repo_relative_path') - if empty(file_path_relative_to_repo_root) - let dir_path_relative_to_repo_root = gitgutter#utility#system(gitgutter#utility#command_in_directory_of_file(g:gitgutter_git_executable.' rev-parse --show-prefix')) - let dir_path_relative_to_repo_root = gitgutter#utility#strip_trailing_new_line(dir_path_relative_to_repo_root) - let file_path_relative_to_repo_root = dir_path_relative_to_repo_root . gitgutter#utility#filename() - call gitgutter#utility#setbufvar(s:bufnr, 'repo_relative_path', file_path_relative_to_repo_root) - endif - return file_path_relative_to_repo_root +" Path of file relative to repo root. +" +" * empty string - not set +" * non-empty string - path +" * -1 - pending +" * -2 - not tracked by git +function! gitgutter#utility#repo_path(bufnr, shellesc) abort + let p = gitgutter#utility#getbufvar(a:bufnr, 'path') + return a:shellesc ? gitgutter#utility#shellescape(p) : p endfunction -function! gitgutter#utility#command_in_directory_of_file(cmd) abort - return 'cd '.gitgutter#utility#shellescape(gitgutter#utility#directory_of_file()).' && '.a:cmd -endfunction +function! gitgutter#utility#set_repo_path(bufnr) abort + " Values of path: + " * non-empty string - path + " * -1 - pending + " * -2 - not tracked by git -function! gitgutter#utility#highlight_name_for_change(text) abort - if a:text ==# 'added' - return 'GitGutterLineAdded' - elseif a:text ==# 'removed' - return 'GitGutterLineRemoved' - elseif a:text ==# 'removed_first_line' - return 'GitGutterLineRemovedFirstLine' - elseif a:text ==# 'modified' - return 'GitGutterLineModified' - elseif a:text ==# 'modified_removed' - return 'GitGutterLineModifiedRemoved' + call gitgutter#utility#setbufvar(a:bufnr, 'path', -1) + let cmd = gitgutter#utility#cd_cmd(a:bufnr, g:gitgutter_git_executable.' ls-files --error-unmatch --full-name '.gitgutter#utility#shellescape(s:filename(a:bufnr))) + + if g:gitgutter_async && gitgutter#async#available() + call gitgutter#async#execute(cmd, a:bufnr, { + \ 'out': {bufnr, path -> gitgutter#utility#setbufvar(bufnr, 'path', s:strip_trailing_new_line(path))}, + \ 'err': {bufnr -> gitgutter#utility#setbufvar(bufnr, 'path', -2)}, + \ }) + else + let path = gitgutter#utility#system(cmd) + if v:shell_error + call gitgutter#utility#setbufvar(a:bufnr, 'path', -2) + else + call gitgutter#utility#setbufvar(a:bufnr, 'path', s:strip_trailing_new_line(path)) + endif endif endfunction -" Dedups list in-place. -" Assumes list has no empty entries. -function! gitgutter#utility#dedup(list) - return filter(sort(a:list), 'index(a:list, v:val, v:key + 1) == -1') +function! gitgutter#utility#cd_cmd(bufnr, cmd) abort + return 'cd '.s:dir(a:bufnr).' && '.a:cmd endfunction -function! gitgutter#utility#strip_trailing_new_line(line) abort +function! s:use_known_shell() abort + if has('unix') && &shell !=# 'sh' + let [s:shell, s:shellcmdflag, s:shellredir] = [&shell, &shellcmdflag, &shellredir] + let &shell = 'sh' + set shellcmdflag=-c shellredir=>%s\ 2>&1 + endif +endfunction + +function! s:restore_shell() abort + if has('unix') && exists('s:shell') + let [&shell, &shellcmdflag, &shellredir] = [s:shell, s:shellcmdflag, s:shellredir] + endif +endfunction + +function! s:abs_path(bufnr, shellesc) + let p = resolve(expand('#'.a:bufnr.':p')) + return a:shellesc ? gitgutter#utility#shellescape(p) : p +endfunction + +function! s:dir(bufnr) abort + return gitgutter#utility#shellescape(fnamemodify(s:abs_path(a:bufnr, 0), ':h')) +endfunction + +" Not shellescaped. +function! s:filename(bufnr) abort + return fnamemodify(s:abs_path(a:bufnr, 0), ':t') +endfunction + +function! s:exists_file(bufnr) abort + return filereadable(s:abs_path(a:bufnr, 0)) +endfunction + +function! s:strip_trailing_new_line(line) abort return substitute(a:line, '\n$', '', '') endfunction - -" True for git v1.7.2+. -function! gitgutter#utility#git_supports_command_line_config_override() abort - call system(g:gitgutter_git_executable.' -c foo.bar=baz --version') - return !v:shell_error -endfunction - -function! gitgutter#utility#stringify(list) abort - return join(a:list, "\n")."\n" -endfunction - -function! gitgutter#utility#use_known_shell() abort - if has('unix') - if &shell !=# 'sh' - let s:shell = &shell - let s:shellcmdflag = &shellcmdflag - let s:shellredir = &shellredir - let &shell = 'sh' - set shellcmdflag=-c - set shellredir=>%s\ 2>&1 - endif - endif -endfunction - -function! gitgutter#utility#restore_shell() abort - if has('unix') - if exists('s:shell') - let &shell = s:shell - let &shellcmdflag = s:shellcmdflag - let &shellredir = s:shellredir - endif - endif -endfunction diff --git a/doc/gitgutter.txt b/doc/gitgutter.txt index 0065d09..22e1933 100644 --- a/doc/gitgutter.txt +++ b/doc/gitgutter.txt @@ -22,11 +22,11 @@ CONTENTS *GitGutterContents* 1. INTRODUCTION *GitGutterIntroduction* *GitGutter* -Vim Git Gutter is a Vim plugin which shows a git diff in the 'gutter' (sign +GitGutter is a Vim plugin which shows a git diff in the 'gutter' (sign column). It shows whether each line has been added, modified, and where lines have been removed. -This is a port of the Git Gutter plugin for Sublime Text 2. +This is a port of the GitGutter plugin for Sublime Text 2. =============================================================================== 2. INSTALLATION *GitGutterInstallation* @@ -301,20 +301,6 @@ Add to your |vimrc| let g:gitgutter_highlight_lines = 1 < -TO STOP VIM-GITGUTTER RUNNING IN REALTIME - -Add to your |vimrc| -> - let g:gitgutter_realtime = 0 -< - -TO STOP VIM-GITGUTTER RUNNING EAGERLY - -Add to your |vimrc| -> - let g:gitgutter_eager = 0 -< - TO TURN OFF ASYNCHRONOUS UPDATES By default diffs are run asynchronously. To run diffs synchronously diff --git a/plugin/gitgutter.vim b/plugin/gitgutter.vim index ad39bc1..277aff7 100644 --- a/plugin/gitgutter.vim +++ b/plugin/gitgutter.vim @@ -33,13 +33,12 @@ call s:set('g:gitgutter_signs', 1) call s:set('g:gitgutter_highlight_lines', 0) call s:set('g:gitgutter_sign_column_always', 0) if g:gitgutter_sign_column_always && exists('&signcolumn') + " Vim 7.4.2201. set signcolumn=yes let g:gitgutter_sign_column_always = 0 call gitgutter#utility#warn('please replace "let g:gitgutter_sign_column_always=1" with "set signcolumn=yes"') endif call s:set('g:gitgutter_override_sign_column_highlight', 1) -call s:set('g:gitgutter_realtime', 1) -call s:set('g:gitgutter_eager', 1) call s:set('g:gitgutter_sign_added', '+') call s:set('g:gitgutter_sign_modified', '~') call s:set('g:gitgutter_sign_removed', '_') @@ -53,15 +52,26 @@ call s:set('g:gitgutter_sign_modified_removed', '~_') call s:set('g:gitgutter_diff_args', '') call s:set('g:gitgutter_diff_base', '') call s:set('g:gitgutter_map_keys', 1) -call s:set('g:gitgutter_avoid_cmd_prompt_on_windows', 1) call s:set('g:gitgutter_async', 1) call s:set('g:gitgutter_log', 0) -call s:set('g:gitgutter_git_executable', 'git') +call s:set('g:gitgutter_git_executable', 'git') if !executable(g:gitgutter_git_executable) call gitgutter#utility#warn('cannot find git. Please set g:gitgutter_git_executable.') endif +call s:set('g:gitgutter_grep', 'grep') +if !empty(g:gitgutter_grep) + if !executable(g:gitgutter_grep) + call gitgutter#utility#warn('cannot find '.g:gitgutter_grep.'. Please set g:gitgutter_grep.') + let g:gitgutter_grep = '' + else + if $GREP_OPTIONS =~# '--color=always' + let g:gitgutter_grep .= ' --color=never' + endif + endif +endif + call gitgutter#highlight#define_sign_column_highlight() call gitgutter#highlight#define_highlights() call gitgutter#highlight#define_signs() @@ -70,8 +80,8 @@ call gitgutter#highlight#define_signs() " Primary functions {{{ -command -bar GitGutterAll call gitgutter#all() -command -bar GitGutter call gitgutter#process_buffer(bufnr(''), 0) +command -bar GitGutterAll call gitgutter#all(1) +command -bar GitGutter call gitgutter#process_buffer(bufnr(''), 1) command -bar GitGutterDisable call gitgutter#disable() command -bar GitGutterEnable call gitgutter#enable() @@ -81,17 +91,17 @@ command -bar GitGutterToggle call gitgutter#toggle() " Line highlights {{{ -command -bar GitGutterLineHighlightsDisable call gitgutter#line_highlights_disable() -command -bar GitGutterLineHighlightsEnable call gitgutter#line_highlights_enable() -command -bar GitGutterLineHighlightsToggle call gitgutter#line_highlights_toggle() +command -bar GitGutterLineHighlightsDisable call gitgutter#highlight#line_disable() +command -bar GitGutterLineHighlightsEnable call gitgutter#highlight#line_enable() +command -bar GitGutterLineHighlightsToggle call gitgutter#highlight#line_toggle() " }}} " Signs {{{ -command -bar GitGutterSignsEnable call gitgutter#signs_enable() -command -bar GitGutterSignsDisable call gitgutter#signs_disable() -command -bar GitGutterSignsToggle call gitgutter#signs_toggle() +command -bar GitGutterSignsEnable call gitgutter#sign#enable() +command -bar GitGutterSignsDisable call gitgutter#sign#disable() +command -bar GitGutterSignsToggle call gitgutter#sign#toggle() " }}} @@ -100,10 +110,10 @@ command -bar GitGutterSignsToggle call gitgutter#signs_toggle() command -bar -count=1 GitGutterNextHunk call gitgutter#hunk#next_hunk() command -bar -count=1 GitGutterPrevHunk call gitgutter#hunk#prev_hunk() -command -bar GitGutterStageHunk call gitgutter#stage_hunk() -command -bar GitGutterUndoHunk call gitgutter#undo_hunk() -command -bar GitGutterRevertHunk echomsg 'GitGutterRevertHunk is deprecated. Use GitGutterUndoHunk'call gitgutter#undo_hunk() -command -bar GitGutterPreviewHunk call gitgutter#preview_hunk() +command -bar GitGutterStageHunk call gitgutter#hunk#stage() +command -bar GitGutterUndoHunk call gitgutter#hunk#undo() +command -bar GitGutterRevertHunk echomsg 'GitGutterRevertHunk is deprecated. Use GitGutterUndoHunk'call gitgutter#hunk#undo() +command -bar GitGutterPreviewHunk call gitgutter#hunk#preview() " Hunk text object onoremap GitGutterTextObjectInnerPending :call gitgutter#hunk#text_object(1) @@ -130,7 +140,8 @@ xnoremap GitGutterTextObjectOuterVisual :call gitgutter#hun " `line` - refers to the line number where the change starts " `count` - refers to the number of lines the change covers function! GitGutterGetHunks() - return gitgutter#utility#is_active() ? gitgutter#hunk#hunks() : [] + let bufnr = bufnr('') + return gitgutter#utility#is_active(bufnr) ? gitgutter#hunk#hunks(bufnr) : [] endfunction " Returns an array that contains a summary of the hunk status for the current @@ -196,34 +207,27 @@ endif augroup gitgutter autocmd! - if g:gitgutter_realtime - autocmd CursorHold,CursorHoldI * call gitgutter#process_buffer(bufnr(''), 1) - endif + autocmd TabEnter * call settabvar(tabpagenr(), 'gitgutter_didtabenter', 1) - if g:gitgutter_eager - autocmd BufWritePost,FileChangedShellPost,ShellCmdPost * call gitgutter#process_buffer(bufnr(''), 0) + autocmd BufEnter * + \ if gettabvar(tabpagenr(), 'gitgutter_didtabenter') | + \ call settabvar(tabpagenr(), 'gitgutter_didtabenter', 0) | + \ call gitgutter#all(0) | + \ else | + \ call gitgutter#init_buffer(bufnr('')) | + \ call gitgutter#process_buffer(bufnr(''), 0) | + \ endif - autocmd BufEnter * - \ if gettabvar(tabpagenr(), 'gitgutter_didtabenter') | - \ call settabvar(tabpagenr(), 'gitgutter_didtabenter', 0) | - \ call gitgutter#all() | - \ else | - \ call gitgutter#process_buffer(bufnr(''), 0) | - \ endif + autocmd CursorHold,CursorHoldI * call gitgutter#process_buffer(bufnr(''), 0) + autocmd FileChangedShellPost,ShellCmdPost * call gitgutter#process_buffer(bufnr(''), 1) - autocmd TabEnter * call settabvar(tabpagenr(), 'gitgutter_didtabenter', 1) + " Ensure that all buffers are processed when opening vim with multiple files, e.g.: + " + " vim -o file1 file2 + autocmd VimEnter * if winnr() != winnr('$') | call gitgutter#all(0) | endif - " Ensure that all buffers are processed when opening vim with multiple files, e.g.: - " - " vim -o file1 file2 - autocmd VimEnter * if winnr() != winnr('$') | :GitGutterAll | endif - - if !has('gui_win32') - autocmd FocusGained * call gitgutter#all() - endif - - else - autocmd BufRead,BufWritePost,FileChangedShellPost * call gitgutter#process_buffer(bufnr(''), 0) + if !has('gui_win32') + autocmd FocusGained * call gitgutter#all(1) endif autocmd ColorScheme * call gitgutter#highlight#define_sign_column_highlight() | call gitgutter#highlight#define_highlights() diff --git a/test/test_gitgutter.vim b/test/test_gitgutter.vim index 1a65ed8..b5d69ec 100644 --- a/test/test_gitgutter.vim +++ b/test/test_gitgutter.vim @@ -64,7 +64,7 @@ endfunction function Test_add_lines() normal ggo* - write + doautocmd CursorHold let expected = ["line=2 id=3000 name=GitGutterLineAdded"] call assert_equal(expected, s:signs('fixture.txt')) @@ -76,7 +76,7 @@ function Test_add_lines_fish() set shell=/usr/local/bin/fish normal ggo* - write + doautocmd CursorHold let expected = ["line=2 id=3000 name=GitGutterLineAdded"] call assert_equal(expected, s:signs('fixture.txt')) @@ -87,7 +87,7 @@ endfunction function Test_modify_lines() normal ggi* - write + doautocmd CursorHold let expected = ["line=1 id=3000 name=GitGutterLineModified"] call assert_equal(expected, s:signs('fixture.txt')) @@ -96,7 +96,7 @@ endfunction function Test_remove_lines() execute '5d' - write + doautocmd CursorHold let expected = ["line=4 id=3000 name=GitGutterLineRemoved"] call assert_equal(expected, s:signs('fixture.txt')) @@ -105,7 +105,7 @@ endfunction function Test_remove_first_lines() execute '1d' - write + doautocmd CursorHold let expected = ["line=1 id=3000 name=GitGutterLineRemovedFirstLine"] call assert_equal(expected, s:signs('fixture.txt')) @@ -115,7 +115,7 @@ endfunction function Test_edit_file_with_same_name_as_a_branch() normal 5Gi* call system('git checkout -b fixture.txt') - write + doautocmd CursorHold let expected = ["line=5 id=3000 name=GitGutterLineModified"] call assert_equal(expected, s:signs('fixture.txt')) @@ -127,7 +127,7 @@ function Test_file_added_to_git() call system('touch '.tmpfile.' && git add '.tmpfile) execute 'edit '.tmpfile normal ihello - write + doautocmd CursorHold let expected = ["line=1 id=3000 name=GitGutterLineAdded"] call assert_equal(expected, s:signs('fileAddedToGit.tmp')) @@ -138,7 +138,7 @@ function Test_filename_with_equals() call system('touch =fixture=.txt && git add =fixture=.txt') edit =fixture=.txt normal ggo* - write + doautocmd CursorHold let expected = [ \ 'line=1 id=3000 name=GitGutterLineAdded', @@ -152,7 +152,7 @@ function Test_filename_with_square_brackets() call system('touch fix[tu]re.txt && git add fix[tu]re.txt') edit fix[tu]re.txt normal ggo* - write + doautocmd CursorHold let expected = [ \ 'line=1 id=3000 name=GitGutterLineAdded', @@ -168,7 +168,7 @@ function Test_follow_symlink() call system('ln -nfs fixture.txt '.tmp) execute 'edit '.tmp 6d - write + doautocmd CursorHold let expected = ['line=5 id=3000 name=GitGutterLineRemoved'] call assert_equal(expected, s:signs('symlink')) @@ -218,26 +218,15 @@ endfunction function Test_orphaned_signs() execute "normal 5GoX\Y" - write + doautocmd CursorHold 6d - write + doautocmd CursorHold let expected = ['line=6 id=3001 name=GitGutterLineAdded'] call assert_equal(expected, s:signs('fixture.txt')) endfunction -function Test_sign_column_always() - let g:gitgutter_sign_column_always=1 - write - - let expected = ['line=9999 id=2999 name=GitGutterDummy'] - call assert_equal(expected, s:signs('fixture.txt')) - - let g:gitgutter_sign_column_always=0 -endfunction - - function Test_untracked_file_outside_repo() let tmp = tempname() call system('touch '.tmp) @@ -301,8 +290,19 @@ function Test_hunk_stage() call assert_equal([], s:signs('fixture.txt')) - call assert_equal([], s:git_diff()) + " Buffer is unsaved + let expected = [ + \ 'diff --git a/fixture.txt b/fixture.txt', + \ 'index ae8e546..f5c6aff 100644', + \ '--- a/fixture.txt', + \ '+++ b/fixture.txt', + \ '@@ -5 +5 @@ d', + \ '-*e', + \ '+e' + \ ] + call assert_equal(expected, s:git_diff()) + " Index has been updated let expected = [ \ 'diff --git a/fixture.txt b/fixture.txt', \ 'index f5c6aff..ae8e546 100644', @@ -313,6 +313,11 @@ function Test_hunk_stage() \ '+*e' \ ] call assert_equal(expected, s:git_diff_staged()) + + " Save the buffer + write + + call assert_equal([], s:git_diff()) endfunction @@ -329,6 +334,31 @@ function Test_hunk_stage_nearby_hunk() \ ] call assert_equal(expected, s:signs('fixture.txt')) + " Buffer is unsaved + let expected = [ + \ 'diff --git a/fixture.txt b/fixture.txt', + \ 'index 53b13df..f5c6aff 100644', + \ '--- a/fixture.txt', + \ '+++ b/fixture.txt', + \ '@@ -3,0 +4 @@ c', + \ '+d', + \ ] + call assert_equal(expected, s:git_diff()) + + " Index has been updated + let expected = [ + \ 'diff --git a/fixture.txt b/fixture.txt', + \ 'index f5c6aff..53b13df 100644', + \ '--- a/fixture.txt', + \ '+++ b/fixture.txt', + \ '@@ -4 +3,0 @@ c', + \ '-d', + \ ] + call assert_equal(expected, s:git_diff_staged()) + + " Save the buffer + write + let expected = [ \ 'diff --git a/fixture.txt b/fixture.txt', \ 'index 53b13df..8fdfda7 100644', @@ -340,16 +370,6 @@ function Test_hunk_stage_nearby_hunk() \ '+z', \ ] call assert_equal(expected, s:git_diff()) - - let expected = [ - \ 'diff --git a/fixture.txt b/fixture.txt', - \ 'index f5c6aff..53b13df 100644', - \ '--- a/fixture.txt', - \ '+++ b/fixture.txt', - \ '@@ -4 +3,0 @@ c', - \ '-d', - \ ] - call assert_equal(expected, s:git_diff_staged()) endfunction @@ -359,7 +379,6 @@ function Test_hunk_undo() normal 5Gi* GitGutterUndoHunk - write " write file so we can verify git diff (--staged) call assert_equal('foo', &shell) let &shell = _shell @@ -374,8 +393,9 @@ function Test_undo_nearby_hunk() execute "normal! 2Gox\y\z" normal 2jdd normal k + doautocmd CursorHold GitGutterUndoHunk - write " write file so we can verify git diff (--staged) + doautocmd CursorHold let expected = [ \ 'line=3 id=3000 name=GitGutterLineAdded', @@ -384,6 +404,13 @@ function Test_undo_nearby_hunk() \ ] call assert_equal(expected, s:signs('fixture.txt')) + call assert_equal([], s:git_diff()) + + call assert_equal([], s:git_diff_staged()) + + " Save the buffer + write + let expected = [ \ 'diff --git a/fixture.txt b/fixture.txt', \ 'index f5c6aff..3fbde56 100644', @@ -396,5 +423,4 @@ function Test_undo_nearby_hunk() \ ] call assert_equal(expected, s:git_diff()) - call assert_equal([], s:git_diff_staged()) endfunction