mirror of
https://github.com/dense-analysis/ale.git
synced 2025-12-24 21:11:28 +08:00
Working directories are now set seperately from the commands so they can later be swapped out when running linters over projects is supported, and also better support filename mapping for running linters on other machines in future.
397 lines
12 KiB
VimL
397 lines
12 KiB
VimL
" Author: w0rp <devw0rp@gmail.com>
|
|
" Description: Functions for fixing code with programs, or other means.
|
|
|
|
let g:ale_fix_on_save_ignore = get(g:, 'ale_fix_on_save_ignore', {})
|
|
let g:ale_filename_mappings = get(g:, 'ale_filename_mappings', {})
|
|
|
|
" Apply fixes queued up for buffers which may be hidden.
|
|
" Vim doesn't let you modify hidden buffers.
|
|
function! ale#fix#ApplyQueuedFixes(buffer) abort
|
|
let l:data = get(g:ale_fix_buffer_data, a:buffer, {'done': 0})
|
|
|
|
if !l:data.done || (!ale#util#HasBuflineApi() && a:buffer isnot bufnr(''))
|
|
return
|
|
endif
|
|
|
|
call remove(g:ale_fix_buffer_data, a:buffer)
|
|
|
|
try
|
|
if l:data.changes_made
|
|
let l:new_lines = ale#util#SetBufferContents(a:buffer, l:data.output)
|
|
|
|
if l:data.should_save
|
|
if a:buffer is bufnr('')
|
|
if empty(&buftype)
|
|
noautocmd :w!
|
|
else
|
|
set nomodified
|
|
endif
|
|
else
|
|
call writefile(l:new_lines, expand('#' . a:buffer . ':p')) " no-custom-checks
|
|
call setbufvar(a:buffer, '&modified', 0)
|
|
endif
|
|
endif
|
|
endif
|
|
catch /E21/
|
|
" If we cannot modify the buffer now, try again later.
|
|
let g:ale_fix_buffer_data[a:buffer] = l:data
|
|
|
|
return
|
|
endtry
|
|
|
|
if l:data.should_save
|
|
let l:should_lint = ale#Var(a:buffer, 'fix_on_save')
|
|
\ && ale#Var(a:buffer, 'lint_on_save')
|
|
else
|
|
let l:should_lint = l:data.changes_made
|
|
endif
|
|
|
|
silent doautocmd <nomodeline> User ALEFixPost
|
|
|
|
" If ALE linting is enabled, check for problems with the file again after
|
|
" fixing problems.
|
|
if g:ale_enabled
|
|
\&& l:should_lint
|
|
\&& !ale#events#QuitRecently(a:buffer)
|
|
call ale#Queue(0, l:data.should_save ? 'lint_file' : '')
|
|
endif
|
|
endfunction
|
|
|
|
function! ale#fix#ApplyFixes(buffer, output) abort
|
|
let l:data = g:ale_fix_buffer_data[a:buffer]
|
|
let l:data.output = a:output
|
|
let l:data.changes_made = l:data.lines_before !=# l:data.output " no-custom-checks
|
|
let l:data.done = 1
|
|
|
|
call ale#command#RemoveManagedFiles(a:buffer)
|
|
|
|
if !bufexists(a:buffer)
|
|
" Remove the buffer data when it doesn't exist.
|
|
call remove(g:ale_fix_buffer_data, a:buffer)
|
|
endif
|
|
|
|
if l:data.changes_made && bufexists(a:buffer)
|
|
let l:lines = getbufline(a:buffer, 1, '$')
|
|
|
|
if l:data.lines_before != l:lines
|
|
call remove(g:ale_fix_buffer_data, a:buffer)
|
|
|
|
if !l:data.ignore_file_changed_errors
|
|
execute 'echoerr ''The file was changed before fixing finished'''
|
|
endif
|
|
|
|
return
|
|
endif
|
|
endif
|
|
|
|
" We can only change the lines of a buffer which is currently open,
|
|
" so try and apply the fixes to the current buffer.
|
|
call ale#fix#ApplyQueuedFixes(a:buffer)
|
|
endfunction
|
|
|
|
function! s:HandleExit(job_info, buffer, job_output, data) abort
|
|
let l:buffer_info = get(g:ale_fix_buffer_data, a:buffer, {})
|
|
|
|
if empty(l:buffer_info)
|
|
return
|
|
endif
|
|
|
|
if a:job_info.read_temporary_file
|
|
let l:output = !empty(a:data.temporary_file)
|
|
\ ? readfile(a:data.temporary_file)
|
|
\ : []
|
|
else
|
|
let l:output = a:job_output
|
|
endif
|
|
|
|
let l:ProcessWith = get(a:job_info, 'process_with', v:null)
|
|
|
|
" Post-process the output with a function if we have one.
|
|
if l:ProcessWith isnot v:null
|
|
let l:output = call(l:ProcessWith, [a:buffer, l:output])
|
|
endif
|
|
|
|
" 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.
|
|
"
|
|
" We'll use the input from before for chained commands.
|
|
if !empty(split(join(l:output)))
|
|
let l:input = l:output
|
|
else
|
|
let l:input = a:job_info.input
|
|
endif
|
|
|
|
call s:RunFixer({
|
|
\ 'buffer': a:buffer,
|
|
\ 'input': l:input,
|
|
\ 'callback_list': a:job_info.callback_list,
|
|
\ 'callback_index': a:job_info.callback_index + 1,
|
|
\})
|
|
endfunction
|
|
|
|
function! s:RunJob(result, options) abort
|
|
if ale#command#IsDeferred(a:result)
|
|
let a:result.result_callback = {x -> s:RunJob(x, a:options)}
|
|
|
|
return
|
|
endif
|
|
|
|
let l:buffer = a:options.buffer
|
|
let l:input = a:options.input
|
|
let l:fixer_name = a:options.fixer_name
|
|
|
|
if a:result is 0 || type(a:result) is v:t_list
|
|
if type(a:result) is v:t_list
|
|
let l:input = a:result
|
|
endif
|
|
|
|
call s:RunFixer({
|
|
\ 'buffer': l:buffer,
|
|
\ 'input': l:input,
|
|
\ 'callback_index': a:options.callback_index + 1,
|
|
\ 'callback_list': a:options.callback_list,
|
|
\})
|
|
|
|
return
|
|
endif
|
|
|
|
let l:command = get(a:result, 'command', '')
|
|
|
|
if empty(l:command)
|
|
" If the command is empty, skip to the next item.
|
|
call s:RunFixer({
|
|
\ 'buffer': l:buffer,
|
|
\ 'input': l:input,
|
|
\ 'callback_index': a:options.callback_index,
|
|
\ 'callback_list': a:options.callback_list,
|
|
\})
|
|
|
|
return
|
|
endif
|
|
|
|
let l:read_temporary_file = get(a:result, 'read_temporary_file', 0)
|
|
let l:read_buffer = get(a:result, 'read_buffer', 1)
|
|
let l:output_stream = get(a:result, 'output_stream', 'stdout')
|
|
let l:cwd = get(a:result, 'cwd', v:null)
|
|
|
|
if l:read_temporary_file
|
|
let l:output_stream = 'none'
|
|
endif
|
|
|
|
let l:Callback = function('s:HandleExit', [{
|
|
\ 'input': l:input,
|
|
\ 'callback_index': a:options.callback_index,
|
|
\ 'callback_list': a:options.callback_list,
|
|
\ 'process_with': get(a:result, 'process_with', v:null),
|
|
\ 'read_temporary_file': l:read_temporary_file,
|
|
\}])
|
|
let l:run_result = ale#command#Run(l:buffer, l:command, l:Callback, {
|
|
\ 'output_stream': l:output_stream,
|
|
\ 'executable': '',
|
|
\ 'read_buffer': l:read_buffer,
|
|
\ 'input': l:input,
|
|
\ 'log_output': 0,
|
|
\ 'cwd': l:cwd,
|
|
\ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:fixer_name),
|
|
\})
|
|
|
|
if empty(l:run_result)
|
|
call s:RunFixer({
|
|
\ 'buffer': l:buffer,
|
|
\ 'input': l:input,
|
|
\ 'callback_index': a:options.callback_index + 1,
|
|
\ 'callback_list': a:options.callback_list,
|
|
\})
|
|
endif
|
|
endfunction
|
|
|
|
function! s:RunFixer(options) abort
|
|
let l:buffer = a:options.buffer
|
|
let l:input = a:options.input
|
|
let l:index = a:options.callback_index
|
|
|
|
if len(a:options.callback_list) <= l:index
|
|
call ale#fix#ApplyFixes(l:buffer, l:input)
|
|
|
|
return
|
|
endif
|
|
|
|
let [l:fixer_name, l:Function] = a:options.callback_list[l:index]
|
|
|
|
" Record new jobs started as fixer jobs.
|
|
call setbufvar(l:buffer, 'ale_job_type', 'fixer')
|
|
|
|
" Regular fixer 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)])
|
|
|
|
call s:RunJob(l:result, {
|
|
\ 'buffer': l:buffer,
|
|
\ 'input': l:input,
|
|
\ 'callback_list': a:options.callback_list,
|
|
\ 'callback_index': l:index,
|
|
\ 'fixer_name': l:fixer_name,
|
|
\})
|
|
endfunction
|
|
|
|
function! s:AddSubCallbacks(full_list, callbacks) abort
|
|
if type(a:callbacks) is v:t_string
|
|
call add(a:full_list, a:callbacks)
|
|
elseif type(a:callbacks) is v:t_list
|
|
call extend(a:full_list, a:callbacks)
|
|
else
|
|
return 0
|
|
endif
|
|
|
|
return 1
|
|
endfunction
|
|
|
|
function! s:IgnoreFixers(callback_list, filetype, config) abort
|
|
if type(a:config) is v:t_list
|
|
let l:ignore_list = a:config
|
|
else
|
|
let l:ignore_list = []
|
|
|
|
for l:part in split(a:filetype , '\.')
|
|
call extend(l:ignore_list, get(a:config, l:part, []))
|
|
endfor
|
|
endif
|
|
|
|
call filter(a:callback_list, 'index(l:ignore_list, v:val) < 0')
|
|
endfunction
|
|
|
|
function! s:GetCallbacks(buffer, fixing_flag, fixers) abort
|
|
if len(a:fixers)
|
|
let l:callback_list = a:fixers
|
|
elseif type(get(b:, 'ale_fixers')) is v:t_list
|
|
" Lists can be used for buffer-local variables only
|
|
let l:callback_list = b:ale_fixers
|
|
else
|
|
" buffer and global options can use dictionaries mapping filetypes to
|
|
" callbacks to run.
|
|
let l:fixers = ale#Var(a:buffer, 'fixers')
|
|
let l:callback_list = []
|
|
let l:matched = 0
|
|
|
|
for l:sub_type in split(&filetype, '\.')
|
|
if s:AddSubCallbacks(l:callback_list, get(l:fixers, l:sub_type))
|
|
let l:matched = 1
|
|
endif
|
|
endfor
|
|
|
|
" If we couldn't find fixers for a filetype, default to '*' fixers.
|
|
if !l:matched
|
|
call s:AddSubCallbacks(l:callback_list, get(l:fixers, '*'))
|
|
endif
|
|
endif
|
|
|
|
if a:fixing_flag is# 'save_file'
|
|
let l:config = ale#Var(a:buffer, 'fix_on_save_ignore')
|
|
|
|
if !empty(l:config)
|
|
call s:IgnoreFixers(l:callback_list, &filetype, l:config)
|
|
endif
|
|
endif
|
|
|
|
let l:corrected_list = []
|
|
|
|
" Variables with capital characters are needed, or Vim will complain about
|
|
" funcref variables.
|
|
for l:Item in l:callback_list
|
|
" Try to capture the names of registered fixer names, so we can use
|
|
" them for filename mapping or other purposes later.
|
|
let l:fixer_name = v:null
|
|
|
|
if type(l:Item) is v:t_string
|
|
let l:Func = ale#fix#registry#GetFunc(l:Item)
|
|
|
|
if !empty(l:Func)
|
|
let l:fixer_name = l:Item
|
|
let l:Item = l:Func
|
|
endif
|
|
endif
|
|
|
|
try
|
|
call add(l:corrected_list, [
|
|
\ l:fixer_name,
|
|
\ ale#util#GetFunction(l:Item)
|
|
\])
|
|
catch /E475/
|
|
" Rethrow exceptions for failing to get a function so we can print
|
|
" a friendly message about it.
|
|
throw 'BADNAME ' . v:exception
|
|
endtry
|
|
endfor
|
|
|
|
return l:corrected_list
|
|
endfunction
|
|
|
|
function! ale#fix#InitBufferData(buffer, fixing_flag) abort
|
|
" The 'done' flag tells the function for applying changes when fixing
|
|
" is complete.
|
|
let g:ale_fix_buffer_data[a:buffer] = {
|
|
\ 'lines_before': getbufline(a:buffer, 1, '$'),
|
|
\ 'done': 0,
|
|
\ 'should_save': a:fixing_flag is# 'save_file',
|
|
\ 'ignore_file_changed_errors': a:fixing_flag is# '!',
|
|
\ 'temporary_directory_list': [],
|
|
\}
|
|
endfunction
|
|
|
|
" Accepts an optional argument for what to do when fixing.
|
|
"
|
|
" Returns 0 if no fixes can be applied, and 1 if fixing can be done.
|
|
function! ale#fix#Fix(buffer, fixing_flag, ...) abort
|
|
if a:fixing_flag isnot# ''
|
|
\&& a:fixing_flag isnot# '!'
|
|
\&& a:fixing_flag isnot# 'save_file'
|
|
throw "fixing_flag must be '', '!', or 'save_file'"
|
|
endif
|
|
|
|
try
|
|
let l:callback_list = s:GetCallbacks(a:buffer, a:fixing_flag, a:000)
|
|
catch /E700\|BADNAME/
|
|
if a:fixing_flag isnot# '!'
|
|
let l:function_name = join(split(split(v:exception, ':')[3]))
|
|
let l:echo_message = printf(
|
|
\ 'There is no fixer named `%s`. Check :ALEFixSuggest',
|
|
\ l:function_name,
|
|
\)
|
|
execute 'echom l:echo_message'
|
|
endif
|
|
|
|
return 0
|
|
endtry
|
|
|
|
if empty(l:callback_list)
|
|
if a:fixing_flag is# ''
|
|
execute 'echom ''No fixers have been defined. Try :ALEFixSuggest'''
|
|
endif
|
|
|
|
return 0
|
|
endif
|
|
|
|
call ale#command#StopJobs(a:buffer, 'fixer')
|
|
" Clean up any files we might have left behind from a previous run.
|
|
call ale#command#RemoveManagedFiles(a:buffer)
|
|
call ale#fix#InitBufferData(a:buffer, a:fixing_flag)
|
|
|
|
silent doautocmd <nomodeline> User ALEFixPre
|
|
|
|
call s:RunFixer({
|
|
\ 'buffer': a:buffer,
|
|
\ 'input': g:ale_fix_buffer_data[a:buffer].lines_before,
|
|
\ 'callback_index': 0,
|
|
\ 'callback_list': l:callback_list,
|
|
\})
|
|
|
|
return 1
|
|
endfunction
|
|
|
|
" Set up an autocmd command to try and apply buffer fixes when available.
|
|
augroup ALEBufferFixGroup
|
|
autocmd!
|
|
autocmd BufEnter * call ale#fix#ApplyQueuedFixes(str2nr(expand('<abuf>')))
|
|
augroup END
|