" Author: w0rp " 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 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') 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, \ '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 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(''))) augroup END