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.
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: APIs for working with Asynchronous jobs, with an API normalised
3 " between Vim 8 and NeoVim.
5 " Important functions are described below. They are:
7 " ale#job#Start(command, options) -> job_id
8 " ale#job#IsRunning(job_id) -> 1 if running, 0 otherwise.
11 " A setting for wrapping commands.
12 let g:ale_command_wrapper = get(g:, 'ale_command_wrapper', '')
14 if !has_key(s:, 'job_map')
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 = {}
24 function! s:KillHandler(timer) abort
25 let l:job = remove(s:job_kill_timers, a:timer)
26 call job_stop(l:job, 'kill')
29 function! s:NeoVimCallback(job, data, event) abort
30 let l:info = s:job_map[a:job]
32 if a:event is# 'stdout'
33 let l:info.out_cb_line = ale#util#JoinNeovimOutput(
38 \ ale#util#GetFunction(l:info.out_cb),
40 elseif a:event is# 'stderr'
41 let l:info.err_cb_line = ale#util#JoinNeovimOutput(
46 \ ale#util#GetFunction(l:info.err_cb),
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)
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)
58 call ale#util#GetFunction(l:info.exit_cb)(a:job, a:data)
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)
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))
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)
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))
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)
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, {})
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'
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))
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)
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, {})
122 let l:info.exit_code = a:exit_code
124 " The program can exit before the data has finished being read.
125 if ch_status(job_getchannel(a:job)) is# 'closed'
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)
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)
140 function! ale#job#ParseVim8ProcessID(job_string) abort
141 return matchstr(a:job_string, '\d\+') + 0
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
150 function! s:PrepareWrappedCommand(original_wrapper, command) abort
151 let l:match = matchlist(a:command, '\v^(.*(\&\&|;)) *(.*)$')
153 let l:command = a:command
156 let l:prefix = l:match[1] . ' '
157 let l:command = l:match[3]
160 let l:format = a:original_wrapper
163 let l:wrapped = substitute(l:format, '%@', ale#Escape(l:command), '')
165 if l:format !~# '%\*'
166 let l:format .= ' %*'
169 let l:wrapped = substitute(l:format, '%\*', l:command, '')
172 return l:prefix . l:wrapped
175 function! ale#job#PrepareCommand(buffer, command) abort
176 let l:wrapper = ale#Var(a:buffer, 'command_wrapper')
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.
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
185 let l:command = !empty(l:wrapper)
186 \ ? s:PrepareWrappedCommand(l:wrapper, a:command)
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
196 if exists('l:ale_shell')
197 let l:shell_arguments = get(b:, 'ale_shell_arguments', get(g:, 'ale_shell_arguments', &shellcmdflag))
199 return split(l:ale_shell) + split(l:shell_arguments) + [l:command]
203 return 'cmd /s/c "' . l:command . '"'
206 if &shell =~? 'fish$\|pwsh$'
207 return ['/bin/sh', '-c', l:command]
210 return split(&shell) + split(&shellcmdflag) + [l:command]
213 " Start a job with options which are agnostic to Vim and NeoVim.
215 " The following options are accepted:
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)
224 let l:job_info = copy(a:options)
225 let l:job_options = {}
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 = ''
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 = ''
238 if has_key(a:options, 'exit_cb')
239 let l:job_options.on_exit = function('s:NeoVimCallback')
242 let l:job_info.job = jobstart(a:command, l:job_options)
243 let l:job_id = l:job_info.job
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,
251 if has_key(a:options, 'out_cb')
252 let l:job_options.out_cb = function('s:VimOutputCallback')
254 " prevent buffering of output and excessive polling in case close_cb is set
255 let l:job_options.out_cb = {->0}
258 if has_key(a:options, 'err_cb')
259 let l:job_options.err_cb = function('s:VimErrorCallback')
261 " prevent buffering of output and excessive polling in case close_cb is set
262 let l:job_options.err_cb = {->0}
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
269 let l:job_options.close_cb = function('s:VimCloseCallback')
270 let l:job_options.exit_cb = function('s:VimExitCallback')
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
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))
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
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
297 let &l:shellcmdflag = '/c'
300 let l:job_id = ale#job#Start(a:command, a:options)
302 let &l:shell = l:shell
303 let &l:shellcmdflag = l:shellcmdflag
309 " Send raw data to the job.
310 function! ale#job#SendRaw(job_id, string) abort
312 call jobsend(a:job_id, a:string)
314 let l:job = s:job_map[a:job_id].job
316 if ch_status(l:job) is# 'open'
317 call ch_sendraw(job_getchannel(l:job), a:string)
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
327 " In NeoVim, if the job isn't running, jobpid() will throw.
328 call jobpid(a:job_id)
333 elseif has_key(s:job_map, a:job_id)
334 let l:job = s:job_map[a:job_id].job
336 return job_status(l:job) is# 'run'
342 function! ale#job#HasOpenChannel(job_id) abort
343 if ale#job#IsRunning(a:job_id)
345 " TODO: Implement a check for NeoVim.
349 " Check if the Job's channel can be written to.
350 return ch_status(s:job_map[a:job_id].job) is# 'open'
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)
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)
369 let l:job = s:job_map[a:job_id].job
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))
377 " Ask nicely for the job to stop.
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