From e607a997cef29923e5847fc434f3d115fcad8e23 Mon Sep 17 00:00:00 2001 From: Andy Stewart Date: Mon, 25 Apr 2016 15:40:49 +0100 Subject: [PATCH] Asynchronous diffs in Vim. --- README.mkd | 7 ++- autoload/gitgutter.vim | 24 -------- autoload/gitgutter/async.vim | 109 +++++++++++++++++++++++++++++++++ autoload/gitgutter/diff.vim | 19 +----- autoload/gitgutter/utility.vim | 15 ----- doc/gitgutter.txt | 4 +- test/test | 2 +- 7 files changed, 119 insertions(+), 61 deletions(-) create mode 100644 autoload/gitgutter/async.vim diff --git a/README.mkd b/README.mkd index e0fc7f8..d0dcca8 100644 --- a/README.mkd +++ b/README.mkd @@ -5,7 +5,8 @@ A Vim plugin which shows a git diff in the 'gutter' (sign column). It shows whe Features: * Shows signs for added, modified, and removed lines. -* Neovim: runs the diffs asynchronously. +* Runs the diffs asynchronously in Vim (7.4.1791+) and NeoVim. + - MacVim in GUI mode runs synchronously because it doesn't support Vim's async operations yet ([macvim#272](/macvim-dev/macvim/issues/272)). * Ensures signs are always as up to date as possible (but without running more than necessary). * Quick jumping between blocks of changed lines ("hunks"). * Stage/undo/preview individual hunks. @@ -202,7 +203,7 @@ You can customise: * Whether or not line highlighting is on initially (defaults to off) * Whether or not vim-gitgutter runs in "realtime" (defaults to yes) * Whether or not vim-gitgutter runs eagerly (defaults to yes) -* Whether or not vim-gitgutter runs asynchronously in NeoVim (defaults to yes) +* Whether or not vim-gitgutter runs asynchronously (defaults to yes) Please note that vim-gitgutter won't override any colours or highlights you've set in your colorscheme. @@ -317,7 +318,7 @@ Add `let g:gitgutter_highlight_lines = 1` to your `~/.vimrc`. #### To turn off asynchronous updates -By default diffs are run asynchronously in NeoVim. To run diffs synchronously instead: +By default diffs are run asynchronously. To run diffs synchronously instead: ```viml let g:gitgutter_async = 0 diff --git a/autoload/gitgutter.vim b/autoload/gitgutter.vim index 7fba0d9..83eb92c 100644 --- a/autoload/gitgutter.vim +++ b/autoload/gitgutter.vim @@ -34,30 +34,6 @@ function! gitgutter#process_buffer(bufnr, realtime) endfunction -function! gitgutter#handle_diff_job(job_id, data, event) - call gitgutter#debug#log('job_id: '.a:job_id.', event: '.a:event) - - if a:event == 'stdout' - " a:data is a list - call gitgutter#utility#job_output_received(a:job_id, 'stdout') - call gitgutter#handle_diff(join(a:data,"\n")."\n") - - elseif a:event == 'exit' - " If the exit event is triggered without a preceding stdout event, - " the diff was empty. - if gitgutter#utility#is_pending_job(a:job_id) - call gitgutter#handle_diff("") - call gitgutter#utility#job_output_received(a:job_id, 'exit') - endif - - else - call gitgutter#hunk#reset() - call gitgutter#utility#job_output_received(a:job_id, 'stderr') - - endif -endfunction - - function! gitgutter#handle_diff(diff) call gitgutter#debug#log(a:diff) diff --git a/autoload/gitgutter/async.vim b/autoload/gitgutter/async.vim new file mode 100644 index 0000000..9a738a1 --- /dev/null +++ b/autoload/gitgutter/async.vim @@ -0,0 +1,109 @@ +let s:jobs = {} +" Async broken on MacVim in GUI mode: +" https://github.com/macvim-dev/macvim/issues/272 +let s:available = has('nvim') || (has('patch-7-4-1791') && !(has('gui_macvim') && has('gui_running'))) + +function! gitgutter#async#available() + return s:available +endfunction + +function! gitgutter#async#execute(cmd) + if has('nvim') + let job_id = jobstart([&shell, &shellcmdflag, a:cmd], { + \ 'on_stdout': function('gitgutter#async#handle_diff_job_nvim'), + \ 'on_stderr': function('gitgutter#async#handle_diff_job_nvim'), + \ 'on_exit': function('gitgutter#async#handle_diff_job_nvim') + \ }) + call gitgutter#debug#log('[nvim job: '.job_id.'] '.a:cmd) + if job_id < 1 + throw 'diff failed' + endif + + " Note that when `cmd` doesn't produce any output, i.e. the diff is empty, + " the `stdout` event is not fired on the job handler. Therefore we keep + " track of the jobs ourselves so we can spot empty diffs. + call s:job_started(job_id) + + else + " Pass a handler for stdout but not for stderr so that errors are + " ignored (and thus signs are not updated; this assumes that an error + " only occurs when a file is not tracked by git). + let job = job_start([&shell, &shellcmdflag, a:cmd], { + \ 'out_cb': 'gitgutter#async#handle_diff_job_vim' + \ }) + " \ 'close_cb': 'gitgutter#handle_diff_job_vim_close' + call gitgutter#debug#log('[vim job: '.string(job_info(job)).'] '.a:cmd) + endif +endfunction + + +function! gitgutter#async#handle_diff_job_nvim(job_id, data, event) + call gitgutter#debug#log('job_id: '.a:job_id.', event: '.a:event) + + if a:event == 'stdout' + " a:data is a list + call s:job_finished(a:job_id) + call gitgutter#handle_diff(join(a:data,"\n")."\n") + + elseif a:event == 'exit' + " If the exit event is triggered without a preceding stdout event, + " the diff was empty. + if s:is_job_started(a:job_id) + call gitgutter#handle_diff("") + call s:job_finished(a:job_id) + endif + + else " a:event is stderr + call gitgutter#hunk#reset() + call s:job_finished(a:job_id) + + endif +endfunction + + +" Channel is in NL mode. +function! gitgutter#async#handle_diff_job_vim(channel, line) + call gitgutter#debug#log('channel: '.a:channel.', line: '.a:line) + + " This seems to be the only way to get info about the channel once closed. + let channel_id = matchstr(a:channel, '\d\+') + + if a:line ==# 'DETACH' " End of job output + call gitgutter#handle_diff(s:job_output(channel_id)) + call s:job_finished(channel_id) + else + call s:accumulate_job_output(channel_id, a:line) + endif +endfunction + + +function! s:job_started(id) + let s:jobs[a:id] = 1 +endfunction + +function! s:is_job_started(id) + return has_key(s:jobs, a:id) +endfunction + +function! s:accumulate_job_output(id, line) + if has_key(s:jobs, a:id) + let s:jobs[a:id] = add(s:jobs[a:id], a:line) + else + let s:jobs[a:id] = [a:line] + endif +endfunction + +" Returns a string +function! s:job_output(id) + if has_key(s:jobs, a:id) + return join(s:jobs[a:id], "\n")."\n" + else + return "" + endif +endfunction + +function! s:job_finished(id) + if has_key(s:jobs, a:id) + unlet s:jobs[a:id] + endif +endfunction diff --git a/autoload/gitgutter/diff.vim b/autoload/gitgutter/diff.vim index 31549cb..7478e1f 100644 --- a/autoload/gitgutter/diff.vim +++ b/autoload/gitgutter/diff.vim @@ -125,23 +125,10 @@ function! gitgutter#diff#run_diff(realtime, preserve_full_diff) let cmd = gitgutter#utility#command_in_directory_of_file(cmd) - if g:gitgutter_async && has('nvim') && !a:preserve_full_diff - let job_id = jobstart([&shell, '-c', cmd], { - \ 'on_stdout': function('gitgutter#handle_diff_job'), - \ 'on_stderr': function('gitgutter#handle_diff_job'), - \ 'on_exit': function('gitgutter#handle_diff_job') - \ }) - call gitgutter#debug#log('[job_id: '.job_id.'] '.cmd) - if job_id < 1 - throw 'diff failed' - endif - - " Note that when `cmd` doesn't produce any output, i.e. the diff is empty, - " the `stdout` event is not fired on the job handler. Therefore we keep - " track of the jobs ourselves so we can spot empty diffs. - call gitgutter#utility#pending_job(job_id) - + if g:gitgutter_async && gitgutter#async#available() && !a:preserve_full_diff + call gitgutter#async#execute(cmd) return 'async' + else let diff = gitgutter#utility#system(cmd) diff --git a/autoload/gitgutter/utility.vim b/autoload/gitgutter/utility.vim index c7b3ccb..3ffb240 100644 --- a/autoload/gitgutter/utility.vim +++ b/autoload/gitgutter/utility.vim @@ -2,7 +2,6 @@ let s:file = '' let s:using_xolox_shell = -1 let s:exit_code = 0 let s:fish = &shell =~# 'fish' -let s:jobs = {} function! gitgutter#utility#warn(message) echohl WarningMsg @@ -170,20 +169,6 @@ function! gitgutter#utility#strip_trailing_new_line(line) return substitute(a:line, '\n$', '', '') endfunction -function! gitgutter#utility#pending_job(job_id) - let s:jobs[a:job_id] = 1 -endfunction - -function! gitgutter#utility#is_pending_job(job_id) - return has_key(s:jobs, a:job_id) -endfunction - -function! gitgutter#utility#job_output_received(job_id, event) - if has_key(s:jobs, a:job_id) - unlet s:jobs[a:job_id] - endif -endfunction - function! gitgutter#utility#git_version() return matchstr(system('git --version'), '[0-9.]\+') endfunction diff --git a/doc/gitgutter.txt b/doc/gitgutter.txt index 86db0df..f7c18f8 100644 --- a/doc/gitgutter.txt +++ b/doc/gitgutter.txt @@ -126,7 +126,7 @@ You can customise: - Whether or not line highlighting is on initially (defaults to off) - Whether or not vim-gitgutter runs in realtime (defaults to yes) - Whether or not vim-gitgutter runs eagerly (defaults to yes) -- Whether or not vim-gitgutter runs asynchronously in NeoVim (defaults to yes) +- Whether or not vim-gitgutter runs asynchronously (defaults to yes) Please note that vim-gitgutter won't override any colours or highlights you've set in your colorscheme. @@ -277,7 +277,7 @@ Add to your |vimrc| TO TURN OFF ASYNCHRONOUS UPDATES -By default diffs are run asynchronously in NeoVim. To run diffs synchronously +By default diffs are run asynchronously. To run diffs synchronously instead: Add to your |vimrc| diff --git a/test/test b/test/test index 6da1262..8e81b33 100755 --- a/test/test +++ b/test/test @@ -4,7 +4,7 @@ VIM="/Applications/MacVim.app/Contents/MacOS/Vim -v" # Execute the tests. for testcase in test*.vim; do - $VIM -N -u NONE -S $testcase -c 'quit!' + $VIM -N -u NONE --cmd 'let g:gitgutter_async=0' -S $testcase -c 'quit!' git reset HEAD fixture.txt > /dev/null git checkout fixture.txt