aboutsummaryrefslogtreecommitdiff
path: root/vim/bundle/vim-gitgutter/autoload/gitgutter/async.vim
blob: 78e725f6d2222dbc1f90b5dd266b6d038fb656d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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('patch-7-4-1826') && !has('gui_running')) ||
      \ (has('patch-7-4-1850') &&  has('gui_running')) ||
      \ (has('patch-7-4-1832') &&  has('gui_macvim'))
      \ )

function! gitgutter#async#available()
  return s:available
endfunction

function! gitgutter#async#execute(cmd) abort
  let bufnr = gitgutter#utility#bufnr()

  if has('nvim')
    if has('unix')
      let command = ["/bin/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 gitgutter#debug#log('[nvim job: '.job_id.', buffer: '.bufnr.'] '.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
    " 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 = ["/bin/sh", "-c", a:cmd]
    elseif has('win32')
      " Help docs recommend {command} be a string on Windows.  But I think
      " they also say that will run the command directly, which I believe would
      " mean the redirection and pipe stuff wouldn't work.
      " let command = "cmd.exe /c ".a:cmd
      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)
  endif
endfunction


function! gitgutter#async#handle_diff_job_nvim(job_id, data, event) abort
  call gitgutter#debug#log('job_id: '.a:job_id.', event: '.a:event.', buffer: '.self.buffer)

  let current_buffer = gitgutter#utility#bufnr()
  call gitgutter#utility#set_buffer(self.buffer)

  if a:event == 'stdout'
    " a:data is a list
    call s:job_finished(a:job_id)
    call gitgutter#handle_diff(gitgutter#utility#stringify(a:data))

  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

  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 current_buffer = gitgutter#utility#bufnr()
  call gitgutter#utility#set_buffer(s:job_buffer(channel_id))

  call gitgutter#handle_diff(s:job_output(channel_id))
  call s:job_finished(channel_id)

  call gitgutter#utility#set_buffer(current_buffer)
endfunction


function! s:channel_id(channel) abort
  " This seems to be the only way to get info about the channel once closed.
  return matchstr(a:channel, '\d\+')
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