diff --git a/autoload/gitgutter/async.vim b/autoload/gitgutter/async.vim index bb72e81..fcd97b4 100644 --- a/autoload/gitgutter/async.vim +++ b/autoload/gitgutter/async.vim @@ -1,14 +1,3 @@ -let s:jobs = {} - -" Nvim has always supported async commands. -" -" Vim introduced async in 7.4.1826. -" -" gVim didn't support aync until 7.4.1850 (though I haven't been able to -" verify this myself). -" -" MacVim-GUI didn't support async until 7.4.1832 (actually commit -" 88f4fe0 but 7.4.1832 was the first subsequent patch release). let s:available = has('nvim') || ( \ has('job') && ( \ (has('patch-7-4-1826') && !has('gui_running')) || @@ -21,187 +10,82 @@ function! gitgutter#async#available() return s:available endfunction + function! gitgutter#async#execute(cmd) abort - let bufnr = gitgutter#utility#bufnr() + let options = { + \ 'stdoutbuffer': [], + \ 'buffer': gitgutter#utility#bufnr() + \ } + let command = s:build_command(a:cmd) if has('nvim') - if has('unix') - let command = ["sh", "-c", a:cmd] - elseif has('win32') - let command = ["cmd.exe", "/c", a:cmd] - else - throw 'unknown os' - endif - " Make the job use a shell while avoiding (un)quoting problems. - let job_id = jobstart(command, { - \ 'buffer': bufnr, - \ '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 jobstart(command, extend(options, { + \ 'on_stdout': function('s:on_stdout_nvim'), + \ 'on_stderr': function('s:on_stderr_nvim'), + \ 'on_exit': function('s:on_exit_nvim') + \ })) + else + call job_start(command, { + \ 'out_cb': function('s:on_stdout_vim', options), + \ 'err_cb': function('s:on_stderr_vim', options), + \ 'close_cb': function('s:on_exit_vim', options) \ }) - call gitgutter#debug#log('[nvim job: '.job_id.', buffer: '.bufnr.'] '.a:cmd) - if job_id < 1 - throw 'diff failed' - endif + endif +endfunction - " 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) +function! s:build_command(cmd) + if has('unix') + return ['sh', '-c', a:cmd] + endif + + if has('win32') + return has('nvim') ? ['cmd.exe', '/c', a:cmd] : 'cmd.exe /c '.a:cmd + endif + + throw 'unknown os' +endfunction + + +function! s:on_stdout_nvim(_job_id, data, _event) dict abort + if empty(self.stdoutbuffer) + let self.stdoutbuffer = a:data else - " Make the job use a shell. - " - " 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). - - if has('unix') - let command = ["sh", "-c", a:cmd] - elseif has('win32') - let command = "cmd.exe /c ".a:cmd - else - throw 'unknown os' - endif - - let job = job_start(command, { - \ 'out_cb': 'gitgutter#async#handle_diff_job_vim', - \ 'close_cb': 'gitgutter#async#handle_diff_job_vim_close' - \ }) - call gitgutter#debug#log('[vim job: '.string(job_info(job)).', buffer: '.bufnr.'] '.a:cmd) - - call s:job_started(s:channel_id(job_getchannel(job)), bufnr) + let self.stdoutbuffer = self.stdoutbuffer[:-2] + + \ [self.stdoutbuffer[-1] . a:data[0]] + + \ a:data[1:] endif endfunction +function! s:on_stderr_nvim(_job_id, _data, _event) dict abort + call s:buffer_exec(self.buffer, function('gitgutter#hunk#reset')) +endfunction -function! gitgutter#async#handle_diff_job_nvim(job_id, data, event) dict abort - call gitgutter#debug#log('job_id: '.a:job_id.', event: '.a:event.', buffer: '.self.buffer) +function! s:on_exit_nvim(_job_id, _data, _event) dict abort + call s:buffer_exec(self.buffer, function('gitgutter#handle_diff', [gitgutter#utility#stringify(self.stdoutbuffer)])) +endfunction - let job_bufnr = self.buffer - if bufexists(job_bufnr) - let current_buffer = gitgutter#utility#bufnr() - call gitgutter#utility#set_buffer(job_bufnr) - if a:event == 'stdout' - " a:data is a list - call s:job_finished(a:job_id) - if gitgutter#utility#is_active() - call gitgutter#handle_diff(gitgutter#utility#stringify(a:data)) - endif +function! s:on_stdout_vim(_channel, data) dict abort + call add(self.stdoutbuffer, a:data) +endfunction - 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) - if gitgutter#utility#is_active() - call gitgutter#handle_diff("") - endif - call s:job_finished(a:job_id) - endif +function! s:on_stderr_vim(_channel, _data) dict abort + call s:buffer_exec(self.buffer, function('gitgutter#hunk#reset')) +endfunction - else " a:event is stderr - call gitgutter#hunk#reset() - call s:job_finished(a:job_id) +function! s:on_exit_vim(_channel) dict abort + call s:buffer_exec(self.buffer, function('gitgutter#handle_diff', [gitgutter#utility#stringify(self.stdoutbuffer)])) +endfunction - endif - call gitgutter#utility#set_buffer(current_buffer) - else - call s:job_finished(a:job_id) +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) endfunction - - -" Channel is in NL mode. -function! gitgutter#async#handle_diff_job_vim(channel, line) abort - call gitgutter#debug#log('channel: '.a:channel.', line: '.a:line) - - call s:accumulate_job_output(s:channel_id(a:channel), a:line) -endfunction - -function! gitgutter#async#handle_diff_job_vim_close(channel) abort - call gitgutter#debug#log('channel: '.a:channel) - - let channel_id = s:channel_id(a:channel) - let job_bufnr = s:job_buffer(channel_id) - - if bufexists(job_bufnr) - let current_buffer = gitgutter#utility#bufnr() - call gitgutter#utility#set_buffer(job_bufnr) - - if gitgutter#utility#is_active() - call gitgutter#handle_diff(s:job_output(channel_id)) - endif - - call gitgutter#utility#set_buffer(current_buffer) - endif - call s:job_finished(channel_id) -endfunction - - -function! s:channel_id(channel) abort - return ch_info(a:channel)['id'] -endfunction - - -" -" Keep track of jobs. -" -" nvim: receives all the job's output at once so we don't need to accumulate -" it ourselves. We can pass the buffer number into the job so we don't need -" to track that either. -" -" s:jobs {} -> key: job's id, value: anything truthy -" -" vim: receives the job's output line by line so we need to accumulate it. -" We also need to keep track of the buffer the job is running for. -" Vim job's don't have an id. Instead we could use the external process's id -" or the channel's id (there seems to be 1 channel per job). Arbitrarily -" choose the channel's id. -" -" s:jobs {} -> key: channel's id, value: {} key: output, value: [] job's output -" key: buffer: value: buffer number - - -" nvim: -" id: job's id -" -" vim: -" id: channel's id -" arg: buffer number -function! s:job_started(id, ...) abort - if a:0 " vim - let s:jobs[a:id] = {'output': [], 'buffer': a:1} - else " nvim - let s:jobs[a:id] = 1 - endif -endfunction - -function! s:is_job_started(id) abort - return has_key(s:jobs, a:id) -endfunction - -function! s:accumulate_job_output(id, line) abort - call add(s:jobs[a:id].output, a:line) -endfunction - -" Returns a string -function! s:job_output(id) abort - if has_key(s:jobs, a:id) - return gitgutter#utility#stringify(s:jobs[a:id].output) - else - return "" - endif -endfunction - -function! s:job_buffer(id) abort - return s:jobs[a:id].buffer -endfunction - -function! s:job_finished(id) abort - if has_key(s:jobs, a:id) - unlet s:jobs[a:id] - endif -endfunction -