342 lines
11 KiB
VimL
342 lines
11 KiB
VimL
" Author: w0rp <devw0rp@gmail.com>
|
|
" Description: Backend execution and job management
|
|
" Executes linters in the background, using NeoVim or Vim 8 jobs
|
|
|
|
" Stores information for each job including:
|
|
"
|
|
" linter: The linter dictionary for the job.
|
|
" buffer: The buffer number for the job.
|
|
" output: The array of lines for the output of the job.
|
|
let s:job_info_map = {}
|
|
|
|
function! s:GetJobID(job) abort
|
|
if has('nvim')
|
|
"In NeoVim, job values are just IDs.
|
|
return a:job
|
|
endif
|
|
|
|
" In Vim 8, the job is a special variable, and we open a channel for each
|
|
" job. We'll use the ID of the channel instead as the job ID.
|
|
return ch_info(job_getchannel(a:job)).id
|
|
endfunction
|
|
|
|
function! ale#engine#InitBufferInfo(buffer) abort
|
|
if !has_key(g:ale_buffer_info, a:buffer)
|
|
let g:ale_buffer_info[a:buffer] = {
|
|
\ 'job_list': [],
|
|
\ 'should_reset': 1,
|
|
\ 'dummy_sign_set': 0,
|
|
\ 'loclist': [],
|
|
\}
|
|
endif
|
|
endfunction
|
|
|
|
function! ale#engine#ClearJob(job) abort
|
|
let l:job_id = s:GetJobID(a:job)
|
|
let l:linter = s:job_info_map[l:job_id].linter
|
|
|
|
if has('nvim')
|
|
call jobstop(a:job)
|
|
else
|
|
" We must close the channel for reading the buffer if it is open
|
|
" when stopping a job. Otherwise, we will get errors in the status line.
|
|
if ch_status(job_getchannel(a:job)) ==# 'open'
|
|
call ch_close_in(job_getchannel(a:job))
|
|
endif
|
|
|
|
call job_stop(a:job)
|
|
endif
|
|
|
|
call remove(s:job_info_map, l:job_id)
|
|
endfunction
|
|
|
|
function! s:StopPreviousJobs(buffer, linter) abort
|
|
let l:new_job_list = []
|
|
|
|
for l:job in g:ale_buffer_info[a:buffer].job_list
|
|
let l:job_id = s:GetJobID(l:job)
|
|
|
|
if has_key(s:job_info_map, l:job_id)
|
|
\&& s:job_info_map[l:job_id].linter.name ==# a:linter.name
|
|
" Stop jobs which match the buffer and linter.
|
|
call ale#engine#ClearJob(l:job)
|
|
else
|
|
" Keep other jobs in the list.
|
|
call add(l:new_job_list, l:job)
|
|
endif
|
|
endfor
|
|
|
|
" Update the list, removing the previously run job.
|
|
let g:ale_buffer_info[a:buffer].job_list = l:new_job_list
|
|
endfunction
|
|
|
|
function! s:GatherOutput(job, data) abort
|
|
let l:job_id = s:GetJobID(a:job)
|
|
|
|
if !has_key(s:job_info_map, l:job_id)
|
|
return
|
|
endif
|
|
|
|
call extend(s:job_info_map[l:job_id].output, a:data)
|
|
endfunction
|
|
|
|
function! s:GatherOutputVim(channel, data) abort
|
|
call s:GatherOutput(ch_getjob(a:channel), [a:data])
|
|
endfunction
|
|
|
|
function! s:GatherOutputNeoVim(job, data, event) abort
|
|
call s:GatherOutput(a:job, a:data)
|
|
endfunction
|
|
|
|
function! s:HandleExit(job) abort
|
|
if a:job ==# 'no process'
|
|
" Stop right away when the job is not valid in Vim 8.
|
|
return
|
|
endif
|
|
|
|
let l:job_id = s:GetJobID(a:job)
|
|
|
|
if !has_key(s:job_info_map, l:job_id)
|
|
return
|
|
endif
|
|
|
|
let l:job_info = s:job_info_map[l:job_id]
|
|
let l:linter = l:job_info.linter
|
|
let l:output = l:job_info.output
|
|
let l:buffer = l:job_info.buffer
|
|
|
|
" Call the same function for stopping jobs again to clean up the job
|
|
" which just closed.
|
|
call s:StopPreviousJobs(l:buffer, l:linter)
|
|
|
|
let l:linter_loclist = ale#util#GetFunction(l:linter.callback)(l:buffer, l:output)
|
|
|
|
" Make some adjustments to the loclists to fix common problems.
|
|
call s:FixLocList(l:buffer, l:linter_loclist)
|
|
|
|
for l:item in l:linter_loclist
|
|
let l:item.linter_name = l:linter.name
|
|
endfor
|
|
|
|
if g:ale_buffer_info[l:buffer].should_reset
|
|
" Set the flag for resetting the loclist to 0 again, so we won't
|
|
" empty the list later.
|
|
let g:ale_buffer_info[l:buffer].should_reset = 0
|
|
let g:ale_buffer_info[l:buffer].loclist = []
|
|
endif
|
|
|
|
" Add the loclist items from the linter.
|
|
call extend(g:ale_buffer_info[l:buffer].loclist, l:linter_loclist)
|
|
|
|
" Sort the loclist again.
|
|
" We need a sorted list so we can run a binary search against it
|
|
" for efficient lookup of the messages in the cursor handler.
|
|
call sort(g:ale_buffer_info[l:buffer].loclist, 'ale#util#LocItemCompare')
|
|
|
|
if g:ale_set_loclist
|
|
call setloclist(0, g:ale_buffer_info[l:buffer].loclist)
|
|
endif
|
|
|
|
if g:ale_set_signs
|
|
call ale#sign#SetSigns(l:buffer, g:ale_buffer_info[l:buffer].loclist)
|
|
endif
|
|
|
|
if exists('*ale#statusline#Update')
|
|
" Don't load/run if not already loaded.
|
|
call ale#statusline#Update(l:buffer, g:ale_buffer_info[l:buffer].loclist)
|
|
endif
|
|
|
|
" Call user autocommands. This allows users to hook into ALE's lint cycle.
|
|
silent doautocmd User ALELint
|
|
|
|
" Mark line 200, column 17 with a squiggly line or something
|
|
" matchadd('ALEError', '\%200l\%17v')
|
|
endfunction
|
|
|
|
function! s:HandleExitNeoVim(job, data, event) abort
|
|
call s:HandleExit(a:job)
|
|
endfunction
|
|
|
|
function! s:HandleExitVim(channel) abort
|
|
call s:HandleExit(ch_getjob(a:channel))
|
|
endfunction
|
|
|
|
function! s:FixLocList(buffer, loclist) abort
|
|
" Some errors have line numbers beyond the end of the file,
|
|
" so we need to adjust them so they set the error at the last line
|
|
" of the file instead.
|
|
let l:last_line_number = ale#util#GetLineCount(a:buffer)
|
|
|
|
for l:item in a:loclist
|
|
if l:item.lnum == 0
|
|
" When errors appear at line 0, put them at line 1 instead.
|
|
let l:item.lnum = 1
|
|
elseif l:item.lnum > l:last_line_number
|
|
let l:item.lnum = l:last_line_number
|
|
endif
|
|
endfor
|
|
endfunction
|
|
|
|
function! ale#engine#Invoke(buffer, linter) abort
|
|
" Stop previous jobs for the same linter.
|
|
call s:StopPreviousJobs(a:buffer, a:linter)
|
|
|
|
if has_key(a:linter, 'command_callback')
|
|
" If there is a callback for generating a command, call that instead.
|
|
let l:command = ale#util#GetFunction(a:linter.command_callback)(a:buffer)
|
|
else
|
|
let l:command = a:linter.command
|
|
endif
|
|
|
|
if l:command =~# '%s'
|
|
" If there is a '%s' in the command string, replace it with the name
|
|
" of the file.
|
|
let l:command = printf(l:command, shellescape(fnamemodify(bufname(a:buffer), ':p')))
|
|
endif
|
|
|
|
if has('nvim')
|
|
if a:linter.output_stream ==# 'stderr'
|
|
" Read from stderr instead of stdout.
|
|
let l:job = jobstart(l:command, {
|
|
\ 'on_stderr': 's:GatherOutputNeoVim',
|
|
\ 'on_exit': 's:HandleExitNeoVim',
|
|
\})
|
|
elseif a:linter.output_stream ==# 'both'
|
|
let l:job = jobstart(l:command, {
|
|
\ 'on_stdout': 's:GatherOutputNeoVim',
|
|
\ 'on_stderr': 's:GatherOutputNeoVim',
|
|
\ 'on_exit': 's:HandleExitNeoVim',
|
|
\})
|
|
else
|
|
let l:job = jobstart(l:command, {
|
|
\ 'on_stdout': 's:GatherOutputNeoVim',
|
|
\ 'on_exit': 's:HandleExitNeoVim',
|
|
\})
|
|
endif
|
|
else
|
|
let l:job_options = {
|
|
\ 'in_mode': 'nl',
|
|
\ 'out_mode': 'nl',
|
|
\ 'err_mode': 'nl',
|
|
\ 'close_cb': function('s:HandleExitVim'),
|
|
\}
|
|
|
|
if a:linter.output_stream ==# 'stderr'
|
|
" Read from stderr instead of stdout.
|
|
let l:job_options.err_cb = function('s:GatherOutputVim')
|
|
elseif a:linter.output_stream ==# 'both'
|
|
" Read from both streams.
|
|
let l:job_options.out_cb = function('s:GatherOutputVim')
|
|
let l:job_options.err_cb = function('s:GatherOutputVim')
|
|
else
|
|
let l:job_options.out_cb = function('s:GatherOutputVim')
|
|
endif
|
|
|
|
if has('win32')
|
|
" job_start commands on Windows have to be run with cmd /c,
|
|
" othwerwise %PATHTEXT% will not be used to programs ending int
|
|
" .cmd, .bat, .exe, etc.
|
|
let l:command = 'cmd /c ' . l:command
|
|
else
|
|
" Execute the command with the shell, to fix escaping issues.
|
|
let l:command = split(&shell) + split(&shellcmdflag) + [l:command]
|
|
|
|
" On Unix machines, we can send the Vim buffer directly.
|
|
" This is faster than reading the lines ourselves.
|
|
let l:job_options.in_io = 'buffer'
|
|
let l:job_options.in_buf = a:buffer
|
|
endif
|
|
|
|
" Vim 8 will read the stdin from the file's buffer.
|
|
let l:job = job_start(l:command, l:job_options)
|
|
endif
|
|
|
|
" Only proceed if the job is being run.
|
|
if has('nvim') || (l:job !=# 'no process' && job_status(l:job) ==# 'run')
|
|
" Add the job to the list of jobs, so we can track them.
|
|
call add(g:ale_buffer_info[a:buffer].job_list, l:job)
|
|
|
|
" Store the ID for the job in the map to read back again.
|
|
let s:job_info_map[s:GetJobID(l:job)] = {
|
|
\ 'linter': a:linter,
|
|
\ 'buffer': a:buffer,
|
|
\ 'output': [],
|
|
\}
|
|
|
|
if has('nvim')
|
|
" In NeoVim, we have to send the buffer lines ourselves.
|
|
let l:input = join(getbufline(a:buffer, 1, '$'), "\n") . "\n"
|
|
|
|
call jobsend(l:job, l:input)
|
|
call jobclose(l:job, 'stdin')
|
|
elseif has('win32')
|
|
" On some Vim versions, we have to send the buffer data ourselves.
|
|
let l:input = join(getbufline(a:buffer, 1, '$'), "\n") . "\n"
|
|
let l:channel = job_getchannel(l:job)
|
|
|
|
if ch_status(l:channel) ==# 'open'
|
|
call ch_sendraw(l:channel, l:input)
|
|
call ch_close_in(l:channel)
|
|
endif
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
" Given a buffer number, return the warnings and errors for a given buffer.
|
|
function! ale#engine#GetLoclist(buffer) abort
|
|
if !has_key(g:ale_buffer_info, a:buffer)
|
|
return []
|
|
endif
|
|
|
|
return g:ale_buffer_info[a:buffer].loclist
|
|
endfunction
|
|
|
|
" This function can be called with a timeout to wait for all jobs to finish.
|
|
" If the jobs to not finish in the given number of milliseconds,
|
|
" an exception will be thrown.
|
|
"
|
|
" The time taken will be a very rough approximation, and more time may be
|
|
" permitted than is specified.
|
|
function! ale#engine#WaitForJobs(deadline) abort
|
|
let l:start_time = system('date +%s%3N') + 0
|
|
|
|
if l:start_time == 0
|
|
throw 'Failed to read milliseconds from the clock!'
|
|
endif
|
|
|
|
let l:job_list = []
|
|
|
|
" Gather all of the jobs from every buffer.
|
|
for l:info in values(g:ale_buffer_info)
|
|
call extend(l:job_list, l:info.job_list)
|
|
endfor
|
|
|
|
let l:should_wait_more = 1
|
|
|
|
while l:should_wait_more
|
|
let l:should_wait_more = 0
|
|
|
|
for l:job in l:job_list
|
|
if job_status(l:job) ==# 'run'
|
|
let l:now = system('date +%s%3N') + 0
|
|
|
|
if l:now - l:start_time > a:deadline
|
|
" Stop waiting after a timeout, so we don't wait forever.
|
|
throw 'Jobs did not complete on time!'
|
|
endif
|
|
|
|
" Wait another 10 milliseconds
|
|
let l:should_wait_more = 1
|
|
sleep 10ms
|
|
break
|
|
endif
|
|
endfor
|
|
endwhile
|
|
|
|
" Sleep for a small amount of time after all jobs finish.
|
|
" This seems to be enough to let handlers after jobs end run, and
|
|
" prevents the occasional failure where this function exits after jobs
|
|
" end, but before handlers are run.
|
|
sleep 10ms
|
|
endfunction
|