]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/job.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / job.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: APIs for working with Asynchronous jobs, with an API normalised
3 " between Vim 8 and NeoVim.
4 "
5 " Important functions are described below. They are:
6 "
7 "   ale#job#Start(command, options) -> job_id
8 "   ale#job#IsRunning(job_id) -> 1 if running, 0 otherwise.
9 "   ale#job#Stop(job_id)
10
11 " A setting for wrapping commands.
12 let g:ale_command_wrapper = get(g:, 'ale_command_wrapper', '')
13
14 if !has_key(s:, 'job_map')
15     let s:job_map = {}
16 endif
17
18 " A map from timer IDs to jobs, for tracking jobs that need to be killed
19 " with SIGKILL if they don't terminate right away.
20 if !has_key(s:, 'job_kill_timers')
21     let s:job_kill_timers = {}
22 endif
23
24 function! s:KillHandler(timer) abort
25     let l:job = remove(s:job_kill_timers, a:timer)
26     call job_stop(l:job, 'kill')
27 endfunction
28
29 function! s:NeoVimCallback(job, data, event) abort
30     let l:info = s:job_map[a:job]
31
32     if a:event is# 'stdout'
33         let l:info.out_cb_line = ale#util#JoinNeovimOutput(
34         \   a:job,
35         \   l:info.out_cb_line,
36         \   a:data,
37         \   l:info.mode,
38         \   ale#util#GetFunction(l:info.out_cb),
39         \)
40     elseif a:event is# 'stderr'
41         let l:info.err_cb_line = ale#util#JoinNeovimOutput(
42         \   a:job,
43         \   l:info.err_cb_line,
44         \   a:data,
45         \   l:info.mode,
46         \   ale#util#GetFunction(l:info.err_cb),
47         \)
48     else
49         if has_key(l:info, 'out_cb') && !empty(l:info.out_cb_line)
50             call ale#util#GetFunction(l:info.out_cb)(a:job, l:info.out_cb_line)
51         endif
52
53         if has_key(l:info, 'err_cb') && !empty(l:info.err_cb_line)
54             call ale#util#GetFunction(l:info.err_cb)(a:job, l:info.err_cb_line)
55         endif
56
57         try
58             call ale#util#GetFunction(l:info.exit_cb)(a:job, a:data)
59         finally
60             " Automatically forget about the job after it's done.
61             if has_key(s:job_map, a:job)
62                 call remove(s:job_map, a:job)
63             endif
64         endtry
65     endif
66 endfunction
67
68 function! s:VimOutputCallback(channel, data) abort
69     let l:job = ch_getjob(a:channel)
70     let l:job_id = ale#job#ParseVim8ProcessID(string(l:job))
71
72     " Only call the callbacks for jobs which are valid.
73     if l:job_id > 0 && has_key(s:job_map, l:job_id)
74         call ale#util#GetFunction(s:job_map[l:job_id].out_cb)(l:job_id, a:data)
75     endif
76 endfunction
77
78 function! s:VimErrorCallback(channel, data) abort
79     let l:job = ch_getjob(a:channel)
80     let l:job_id = ale#job#ParseVim8ProcessID(string(l:job))
81
82     " Only call the callbacks for jobs which are valid.
83     if l:job_id > 0 && has_key(s:job_map, l:job_id)
84         call ale#util#GetFunction(s:job_map[l:job_id].err_cb)(l:job_id, a:data)
85     endif
86 endfunction
87
88 function! s:VimCloseCallback(channel) abort
89     let l:job = ch_getjob(a:channel)
90     let l:job_id = ale#job#ParseVim8ProcessID(string(l:job))
91     let l:info = get(s:job_map, l:job_id, {})
92
93     if empty(l:info)
94         return
95     endif
96
97     " job_status() can trigger the exit handler.
98     " The channel can close before the job has exited.
99     if job_status(l:job) is# 'dead'
100         try
101             if !empty(l:info) && has_key(l:info, 'exit_cb')
102                 " We have to remove the callback, so we don't call it twice.
103                 call ale#util#GetFunction(remove(l:info, 'exit_cb'))(l:job_id, get(l:info, 'exit_code', 1))
104             endif
105         finally
106             " Automatically forget about the job after it's done.
107             if has_key(s:job_map, l:job_id)
108                 call remove(s:job_map, l:job_id)
109             endif
110         endtry
111     endif
112 endfunction
113
114 function! s:VimExitCallback(job, exit_code) abort
115     let l:job_id = ale#job#ParseVim8ProcessID(string(a:job))
116     let l:info = get(s:job_map, l:job_id, {})
117
118     if empty(l:info)
119         return
120     endif
121
122     let l:info.exit_code = a:exit_code
123
124     " The program can exit before the data has finished being read.
125     if ch_status(job_getchannel(a:job)) is# 'closed'
126         try
127             if !empty(l:info) && has_key(l:info, 'exit_cb')
128                 " We have to remove the callback, so we don't call it twice.
129                 call ale#util#GetFunction(remove(l:info, 'exit_cb'))(l:job_id, a:exit_code)
130             endif
131         finally
132             " Automatically forget about the job after it's done.
133             if has_key(s:job_map, l:job_id)
134                 call remove(s:job_map, l:job_id)
135             endif
136         endtry
137     endif
138 endfunction
139
140 function! ale#job#ParseVim8ProcessID(job_string) abort
141     return matchstr(a:job_string, '\d\+') + 0
142 endfunction
143
144 function! ale#job#ValidateArguments(command, options) abort
145     if a:options.mode isnot# 'nl' && a:options.mode isnot# 'raw'
146         throw 'Invalid mode: ' . a:options.mode
147     endif
148 endfunction
149
150 function! s:PrepareWrappedCommand(original_wrapper, command) abort
151     let l:match = matchlist(a:command, '\v^(.*(\&\&|;)) *(.*)$')
152     let l:prefix = ''
153     let l:command = a:command
154
155     if !empty(l:match)
156         let l:prefix = l:match[1] . ' '
157         let l:command = l:match[3]
158     endif
159
160     let l:format = a:original_wrapper
161
162     if l:format =~# '%@'
163         let l:wrapped = substitute(l:format, '%@', ale#Escape(l:command), '')
164     else
165         if l:format !~# '%\*'
166             let l:format .= ' %*'
167         endif
168
169         let l:wrapped = substitute(l:format, '%\*', l:command, '')
170     endif
171
172     return l:prefix . l:wrapped
173 endfunction
174
175 function! ale#job#PrepareCommand(buffer, command) abort
176     let l:wrapper = ale#Var(a:buffer, 'command_wrapper')
177
178     " The command will be executed in a subshell. This fixes a number of
179     " issues, including reading the PATH variables correctly, %PATHEXT%
180     " expansion on Windows, etc.
181     "
182     " NeoVim handles this issue automatically if the command is a String,
183     " but we'll do this explicitly, so we use the same exact command for both
184     " versions.
185     let l:command = !empty(l:wrapper)
186     \ ? s:PrepareWrappedCommand(l:wrapper, a:command)
187     \ : a:command
188
189     " If a custom shell is specified, use that.
190     if exists('b:ale_shell')
191         let l:ale_shell = b:ale_shell
192     elseif exists('g:ale_shell')
193         let l:ale_shell = g:ale_shell
194     endif
195
196     if exists('l:ale_shell')
197         let l:shell_arguments = get(b:, 'ale_shell_arguments', get(g:, 'ale_shell_arguments', &shellcmdflag))
198
199         return split(l:ale_shell) + split(l:shell_arguments) + [l:command]
200     endif
201
202     if has('win32')
203         return 'cmd /s/c "' . l:command . '"'
204     endif
205
206     if &shell =~? 'fish$\|pwsh$'
207         return ['/bin/sh', '-c', l:command]
208     endif
209
210     return split(&shell) + split(&shellcmdflag) + [l:command]
211 endfunction
212
213 " Start a job with options which are agnostic to Vim and NeoVim.
214 "
215 " The following options are accepted:
216 "
217 " out_cb  - A callback for receiving stdin.  Arguments: (job_id, data)
218 " err_cb  - A callback for receiving stderr. Arguments: (job_id, data)
219 " exit_cb - A callback for program exit.     Arguments: (job_id, status_code)
220 " mode    - A mode for I/O. Can be 'nl' for split lines or 'raw'.
221 function! ale#job#Start(command, options) abort
222     call ale#job#ValidateArguments(a:command, a:options)
223
224     let l:job_info = copy(a:options)
225     let l:job_options = {}
226
227     if has('nvim')
228         if has_key(a:options, 'out_cb')
229             let l:job_options.on_stdout = function('s:NeoVimCallback')
230             let l:job_info.out_cb_line = ''
231         endif
232
233         if has_key(a:options, 'err_cb')
234             let l:job_options.on_stderr = function('s:NeoVimCallback')
235             let l:job_info.err_cb_line = ''
236         endif
237
238         if has_key(a:options, 'exit_cb')
239             let l:job_options.on_exit = function('s:NeoVimCallback')
240         endif
241
242         let l:job_info.job = jobstart(a:command, l:job_options)
243         let l:job_id = l:job_info.job
244     else
245         let l:job_options = {
246         \   'in_mode': l:job_info.mode,
247         \   'out_mode': l:job_info.mode,
248         \   'err_mode': l:job_info.mode,
249         \}
250
251         if has_key(a:options, 'out_cb')
252             let l:job_options.out_cb = function('s:VimOutputCallback')
253         else
254             " prevent buffering of output and excessive polling in case close_cb is set
255             let l:job_options.out_cb = {->0}
256         endif
257
258         if has_key(a:options, 'err_cb')
259             let l:job_options.err_cb = function('s:VimErrorCallback')
260         else
261             " prevent buffering of output and excessive polling in case close_cb is set
262             let l:job_options.err_cb = {->0}
263         endif
264
265         if has_key(a:options, 'exit_cb')
266             " Set a close callback to which simply calls job_status()
267             " when the channel is closed, which can trigger the exit callback
268             " earlier on.
269             let l:job_options.close_cb = function('s:VimCloseCallback')
270             let l:job_options.exit_cb = function('s:VimExitCallback')
271         endif
272
273         " Use non-blocking writes for Vim versions that support the option.
274         if has('patch-8.1.350')
275             let l:job_options.noblock = 1
276         endif
277
278         " Vim 8 will read the stdin from the file's buffer.
279         let l:job_info.job = job_start(a:command, l:job_options)
280         let l:job_id = ale#job#ParseVim8ProcessID(string(l:job_info.job))
281     endif
282
283     if l:job_id > 0
284         " Store the job in the map for later only if we can get the ID.
285         let s:job_map[l:job_id] = l:job_info
286     endif
287
288     return l:job_id
289 endfunction
290
291 " Force running commands in a Windows CMD command line.
292 " This means the same command syntax works everywhere.
293 function! ale#job#StartWithCmd(command, options) abort
294     let l:shell = &l:shell
295     let l:shellcmdflag = &l:shellcmdflag
296     let &l:shell = 'cmd'
297     let &l:shellcmdflag = '/c'
298
299     try
300         let l:job_id = ale#job#Start(a:command, a:options)
301     finally
302         let &l:shell = l:shell
303         let &l:shellcmdflag = l:shellcmdflag
304     endtry
305
306     return l:job_id
307 endfunction
308
309 " Send raw data to the job.
310 function! ale#job#SendRaw(job_id, string) abort
311     if has('nvim')
312         call jobsend(a:job_id, a:string)
313     else
314         let l:job = s:job_map[a:job_id].job
315
316         if ch_status(l:job) is# 'open'
317             call ch_sendraw(job_getchannel(l:job), a:string)
318         endif
319     endif
320 endfunction
321
322 " Given a job ID, return 1 if the job is currently running.
323 " Invalid job IDs will be ignored.
324 function! ale#job#IsRunning(job_id) abort
325     if has('nvim')
326         try
327             " In NeoVim, if the job isn't running, jobpid() will throw.
328             call jobpid(a:job_id)
329
330             return 1
331         catch
332         endtry
333     elseif has_key(s:job_map, a:job_id)
334         let l:job = s:job_map[a:job_id].job
335
336         return job_status(l:job) is# 'run'
337     endif
338
339     return 0
340 endfunction
341
342 function! ale#job#HasOpenChannel(job_id) abort
343     if ale#job#IsRunning(a:job_id)
344         if has('nvim')
345             " TODO: Implement a check for NeoVim.
346             return 1
347         endif
348
349         " Check if the Job's channel can be written to.
350         return ch_status(s:job_map[a:job_id].job) is# 'open'
351     endif
352
353     return 0
354 endfunction
355
356 " Given a Job ID, stop that job.
357 " Invalid job IDs will be ignored.
358 function! ale#job#Stop(job_id) abort
359     if !has_key(s:job_map, a:job_id)
360         return
361     endif
362
363     if has('nvim')
364         " FIXME: NeoVim kills jobs on a timer, but will not kill any processes
365         " which are child processes on Unix. Some work needs to be done to
366         " kill child processes to stop long-running processes like pylint.
367         silent! call jobstop(a:job_id)
368     else
369         let l:job = s:job_map[a:job_id].job
370
371         " We must close the channel for reading the buffer if it is open
372         " when stopping a job. Otherwise, we will get errors in the status line.
373         if ch_status(job_getchannel(l:job)) is# 'open'
374             call ch_close_in(job_getchannel(l:job))
375         endif
376
377         " Ask nicely for the job to stop.
378         call job_stop(l:job)
379
380         if ale#job#IsRunning(l:job)
381             " Set a 100ms delay for killing the job with SIGKILL.
382             let s:job_kill_timers[timer_start(100, function('s:KillHandler'))] = l:job
383         endif
384     endif
385 endfunction