diff options
-rw-r--r-- | autoload/ale/engine.vim | 88 | ||||
-rw-r--r-- | autoload/ale/linter.vim | 72 |
2 files changed, 124 insertions, 36 deletions
diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 731bacf1..960b993e 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -106,11 +106,17 @@ function! s:HandleExit(job) abort let l:linter = l:job_info.linter let l:output = l:job_info.output let l:buffer = l:job_info.buffer + let l:next_chain_index = l:job_info.next_chain_index " Call the same function for stopping jobs again to clean up the job " which just closed. call s:StopPreviousJobs(l:buffer, l:linter) + if l:next_chain_index < len(get(l:linter, 'command_chain', [])) + call s:InvokeChain(l:buffer, l:linter, l:next_chain_index, l:output) + return + endif + let l:linter_loclist = ale#util#GetFunction(l:linter.callback)(l:buffer, l:output) " Make some adjustments to the loclists to fix common problems. @@ -182,31 +188,27 @@ function! s:FixLocList(buffer, loclist) abort 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 +function! s:RunJob(command, generic_job_options) abort + let l:buffer = a:generic_job_options.buffer + let l:linter = a:generic_job_options.linter + let l:output_stream = a:generic_job_options.output_stream + let l:next_chain_index = a:generic_job_options.next_chain_index + let l:command = a:command 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'))) + let l:command = printf(l:command, shellescape(fnamemodify(bufname(l:buffer), ':p'))) endif if has('nvim') - if a:linter.output_stream ==# 'stderr' + if l: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' + elseif l:output_stream ==# 'both' let l:job = jobstart(l:command, { \ 'on_stdout': 's:GatherOutputNeoVim', \ 'on_stderr': 's:GatherOutputNeoVim', @@ -226,10 +228,10 @@ function! ale#engine#Invoke(buffer, linter) abort \ 'close_cb': function('s:HandleExitVim'), \} - if a:linter.output_stream ==# 'stderr' + if l:output_stream ==# 'stderr' " Read from stderr instead of stdout. let l:job_options.err_cb = function('s:GatherOutputVim') - elseif a:linter.output_stream ==# 'both' + elseif l: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') @@ -249,7 +251,7 @@ function! ale#engine#Invoke(buffer, linter) abort " 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 + let l:job_options.in_buf = l:buffer endif " Vim 8 will read the stdin from the file's buffer. @@ -259,24 +261,25 @@ function! ale#engine#Invoke(buffer, linter) abort " 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) + call add(g:ale_buffer_info[l: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, + \ 'linter': l:linter, + \ 'buffer': l:buffer, \ 'output': [], + \ 'next_chain_index': l:next_chain_index, \} if has('nvim') " In NeoVim, we have to send the buffer lines ourselves. - let l:input = join(getbufline(a:buffer, 1, '$'), "\n") . "\n" + let l:input = join(getbufline(l: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:input = join(getbufline(l:buffer, 1, '$'), "\n") . "\n" let l:channel = job_getchannel(l:job) if ch_status(l:channel) ==# 'open' @@ -287,6 +290,49 @@ function! ale#engine#Invoke(buffer, linter) abort endif endfunction +function! s:InvokeChain(buffer, linter, chain_index, input) abort + let l:output_stream = get(a:linter, 'output_stream', 'stdout') + + if has_key(a:linter, 'command_chain') + " Run a chain of commands, one asychronous command after the other, + " so that many programs can be run in a sequence. + let l:chain_item = a:linter.command_chain[a:chain_index] + + " The chain item can override the output_stream option. + if has_key(l:chain_item) + let l:output_stream = l:chain_item.output_stream + endif + + let l:callback = ale#util#GetFunction(a:linter.callback) + + if a:chain_index == 0 + " The first callback in the chain takes only a buffer number. + let l:command = l:callback(a:buffer) + else + " The second callback in the chain takes some input too. + let l:command = l:callback(a:buffer, a:input) + endif + elseif 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 + + call s:RunJob(l:command, { + \ 'buffer': a:buffer, + \ 'linter': a:linter, + \ 'output_stream': l:output_stream, + \ 'next_chain_index': a:chain_index + 1, + \}) +endfunction + +function! ale#engine#Invoke(buffer, linter) abort + " Stop previous jobs for the same linter. + call s:StopPreviousJobs(a:buffer, a:linter) + call s:InvokeChain(a:buffer, a:linter, 0, []) +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) diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 757e6a40..802d8646 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -25,35 +25,77 @@ function! ale#linter#Reset() abort let s:linters = {} endfunction -function! ale#linter#Define(filetype, linter) abort - if !has_key(s:linters, a:filetype) - let s:linters[a:filetype] = [] +function! s:IsCallback(value) abort + return type(a:value) == type('') || type(a:value) == type(function('type')) +endfunction + +function! ale#linter#PreProcess(linter) abort + if type(a:linter) != type({}) + throw 'The linter object must be a Dictionary' endif - let l:new_linter = { - \ 'name': a:linter.name, - \ 'callback': a:linter.callback, + let l:obj = { + \ 'name': get(a:linter, 'name'), + \ 'callback': get(a:linter, 'callback'), \} + if type(l:obj.name) != type('') + throw '`name` must be defined to name the linter' + endif + + if !s:IsCallback(l:obj.callback) + throw '`callback` must be defined with a callback to accept output' + endif + if has_key(a:linter, 'executable_callback') - let l:new_linter.executable_callback = a:linter.executable_callback + let l:obj.executable_callback = a:linter.executable_callback + + if !s:IsCallback(l:obj.executable_callback) + throw '`executable_callback` must be a callback if defined' + endif + elseif has_key(a:linter, 'executable') + let l:obj.executable = a:linter.executable + + if type(l:obj.executable) != type('') + throw '`executable` must be a string if defined' + endif else - let l:new_linter.executable = a:linter.executable + throw 'Either `executable` or `executable_callback` must be defined' endif if has_key(a:linter, 'command_callback') - let l:new_linter.command_callback = a:linter.command_callback + let l:obj.command_callback = a:linter.command_callback + + if !s:IsCallback(l:obj.command_callback) + throw '`command_callback` must be a callback if defined' + endif + elseif has_key(a:linter, 'command') + let l:obj.command = a:linter.command + + if type(l:obj.command) != type('') + throw '`command` must be a string if defined' + endif else - let l:new_linter.command = a:linter.command + throw 'Either `command`, `executable_callback`, `command_chain` ' + \ . 'must be defined' endif - if has_key(a:linter, 'output_stream') - let l:new_linter.output_stream = a:linter.output_stream - else - let l:new_linter.output_stream = 'stdout' + let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout') + + if type(l:obj.output_stream) != type('') + \|| index(['stdout', 'stderr', 'both'], l:obj.output_stream) < 0 + throw "`output_stream` must be 'stdout', 'stderr', or 'both'" + endif + + return l:obj +endfunction + +function! ale#linter#Define(filetype, linter) abort + if !has_key(s:linters, a:filetype) + let s:linters[a:filetype] = [] endif - " TODO: Assert the value of the output_stream to be something sensible. + let l:new_linter = ale#linter#PreProcess(a:linter) call add(s:linters[a:filetype], l:new_linter) endfunction |