Fix #1086 - Implement command chaining for fixers

This commit is contained in:
w0rp 2017-11-07 23:20:14 +00:00
parent d97924b698
commit 1bf894f48c
3 changed files with 159 additions and 7 deletions

View File

@ -108,17 +108,27 @@ function! s:HandleExit(job_id, exit_code) abort
let l:job_info.output = readfile(l:job_info.file_to_read)
endif
let l:chain_callback = get(l:job_info, 'chain_with', v:null)
" Use the output of the job for changing the file if it isn't empty,
" otherwise skip this job and use the input from before.
let l:input = !empty(l:job_info.output)
"
" We'll use the input from before for chained commands.
let l:input = l:chain_callback is v:null && !empty(l:job_info.output)
\ ? l:job_info.output
\ : l:job_info.input
let l:next_index = l:chain_callback is v:null
\ ? l:job_info.callback_index + 1
\ : l:job_info.callback_index
call s:RunFixer({
\ 'buffer': l:buffer,
\ 'input': l:input,
\ 'output': l:job_info.output,
\ 'callback_list': l:job_info.callback_list,
\ 'callback_index': l:job_info.callback_index + 1,
\ 'callback_index': l:next_index,
\ 'chain_callback': l:chain_callback,
\})
endfunction
@ -172,6 +182,26 @@ function! s:RunJob(options) abort
let l:input = a:options.input
let l:output_stream = a:options.output_stream
let l:read_temporary_file = a:options.read_temporary_file
let l:chain_with = a:options.chain_with
if empty(l:command)
" If there's nothing further to chain the command with, stop here.
if l:chain_with is v:null
return 0
endif
" If there's another chained callback to run, then run that.
call s:RunFixer({
\ 'buffer': l:buffer,
\ 'input': l:input,
\ 'callback_index': a:options.callback_index,
\ 'callback_list': a:options.callback_list,
\ 'chain_callback': l:chain_with,
\ 'output': [],
\})
return 1
endif
let [l:temporary_file, l:command] = ale#command#FormatCommand(l:buffer, l:command, 1)
call s:CreateTemporaryFileForJob(l:buffer, l:temporary_file, l:input)
@ -186,8 +216,9 @@ function! s:RunJob(options) abort
\ 'buffer': l:buffer,
\ 'input': l:input,
\ 'output': [],
\ 'callback_list': a:options.callback_list,
\ 'chain_with': l:chain_with,
\ 'callback_index': a:options.callback_index,
\ 'callback_list': a:options.callback_list,
\}
if l:read_temporary_file
@ -250,13 +281,24 @@ function! s:RunFixer(options) abort
let l:buffer = a:options.buffer
let l:input = a:options.input
let l:index = a:options.callback_index
let l:chain_callback = get(a:options, 'chain_callback', v:null)
while len(a:options.callback_list) > l:index
let l:Function = a:options.callback_list[l:index]
let l:Function = l:chain_callback isnot v:null
\ ? ale#util#GetFunction(l:chain_callback)
\ : a:options.callback_list[l:index]
let l:result = ale#util#FunctionArgCount(l:Function) == 1
\ ? call(l:Function, [l:buffer])
\ : call(l:Function, [l:buffer, copy(l:input)])
if l:chain_callback isnot v:null
" Chained commands accept (buffer, output, [input])
let l:result = ale#util#FunctionArgCount(l:Function) == 2
\ ? call(l:Function, [l:buffer, a:options.output])
\ : call(l:Function, [l:buffer, a:options.output, copy(l:input)])
else
" Chained commands accept (buffer, [input])
let l:result = ale#util#FunctionArgCount(l:Function) == 1
\ ? call(l:Function, [l:buffer])
\ : call(l:Function, [l:buffer, copy(l:input)])
endif
if type(l:result) == type(0) && l:result == 0
" When `0` is returned, skip this item.
@ -271,6 +313,7 @@ function! s:RunFixer(options) abort
\ 'input': l:input,
\ 'output_stream': get(l:result, 'output_stream', 'stdout'),
\ 'read_temporary_file': get(l:result, 'read_temporary_file', 0),
\ 'chain_with': get(l:result, 'chain_with', v:null),
\ 'callback_list': a:options.callback_list,
\ 'callback_index': l:index,
\})

View File

@ -443,6 +443,26 @@ are supported for running the commands.
for commands which need to modify some file on disk in
order to fix files.
`chain_with` An optional key for defining a callback to call next.
The callback must accept two or three arguments,
`(buffer, output)` or `(buffer, output, input)` .
Functions receiving a variable number of arguments will
only receive the first two values. The `output` argument
will contain the lines of output from the command run.
The `input` argument is the List of lines for the
buffer, after applying any previous fixers.
The callback must return the same values returned for
any fixer function. This allows fixer functions to be
chained recursively.
When the command string returned for a fixer is an empty
string, the next command in the chain will still be run.
This allows commands to be skipped, like version checks
that are cached. An empty List will be passed to the
next callback in the chain for the `output`.
*ale-fix-configuration*
Synchronous functions and asynchronous jobs will be run in a sequence for

View File

@ -62,6 +62,49 @@ Before:
return [{'lnum': 1, 'col': 1, 'text': 'xxx'}]
endfunction
function! FirstChainCallback(buffer)
return {'command': 'echo echoline', 'chain_with': 'SecondChainCallback'}
endfunction
function! FirstChainCallbackSkipped(buffer)
return {'command': '', 'chain_with': 'SecondChainCallback'}
endfunction
function! FirstChainCallbackSecondSkipped(buffer)
return {'command': 'echo skipit', 'chain_with': 'SecondChainCallback'}
endfunction
function! SecondChainCallback(buffer, output)
let l:previous_line = empty(a:output)
\ ? 'emptydefault'
\ : join(split(a:output[0]))
if l:previous_line is# 'skipit'
return {'command': '', 'chain_with': 'ThirdChainCallback'}
endif
return {
\ 'command': 'echo ' . l:previous_line,
\ 'chain_with': 'ThirdChainCallback',
\}
endfunction
function! ThirdChainCallback(buffer, output, input)
let l:previous_line = empty(a:output)
\ ? 'thirddefault'
\ : join(split(a:output[0]))
return a:input + [l:previous_line]
endfunction
function! ChainWhereLastIsSkipped(buffer)
return {'command': 'echo echoline', 'chain_with': 'ChainEndSkipped'}
endfunction
function! ChainEndSkipped(buffer, output)
return {'command': ''}
endfunction
function! SetUpLinters()
call ale#linter#Define('testft', {
\ 'name': 'testlinter',
@ -97,6 +140,13 @@ After:
delfunction RemoveLastLine
delfunction RemoveLastLineOneArg
delfunction TestCallback
delfunction FirstChainCallback
delfunction FirstChainCallbackSkipped
delfunction FirstChainCallbackSecondSkipped
delfunction SecondChainCallback
delfunction ThirdChainCallback
delfunction ChainWhereLastIsSkipped
delfunction ChainEndSkipped
delfunction SetUpLinters
delfunction GetLastMessage
@ -470,3 +520,42 @@ Execute(ALE should print a message telling you something isn't a valid fixer whe
ALEFix
AssertEqual 'There is no fixer named `invalidname`. Check :ALEFixSuggest', GetLastMessage()
Execute(Test fixing with chained callbacks):
let g:ale_fixers.testft = ['FirstChainCallback']
ALEFix
Expect(The echoed line should be added):
a
b
c
echoline
Execute(Test fixing with chained callback where the first command is skipped):
let g:ale_fixers.testft = ['FirstChainCallbackSkipped']
ALEFix
Expect(The default line should be added):
a
b
c
emptydefault
Execute(Test fixing with chained callback where the second command is skipped):
let g:ale_fixers.testft = ['FirstChainCallbackSecondSkipped']
ALEFix
Expect(The default line should be added):
a
b
c
thirddefault
Execute(Test fixing with chained callback where the final callback is skipped):
let g:ale_fixers.testft = ['ChainWhereLastIsSkipped']
ALEFix
Expect(The lines should be the same):
a
b
c