All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Functions for fixing code with programs, or other means.
4 let g:ale_fix_on_save_ignore = get(g:, 'ale_fix_on_save_ignore', {})
5 let g:ale_filename_mappings = get(g:, 'ale_filename_mappings', {})
7 " Apply fixes queued up for buffers which may be hidden.
8 " Vim doesn't let you modify hidden buffers.
9 function! ale#fix#ApplyQueuedFixes(buffer) abort
10 let l:data = get(g:ale_fix_buffer_data, a:buffer, {'done': 0})
12 if !l:data.done || (!ale#util#HasBuflineApi() && a:buffer isnot bufnr(''))
16 call remove(g:ale_fix_buffer_data, a:buffer)
19 if l:data.changes_made
20 let l:new_lines = ale#util#SetBufferContents(a:buffer, l:data.output)
23 if a:buffer is bufnr('')
30 call writefile(l:new_lines, expand('#' . a:buffer . ':p')) " no-custom-checks
31 call setbufvar(a:buffer, '&modified', 0)
36 " If we cannot modify the buffer now, try again later.
37 let g:ale_fix_buffer_data[a:buffer] = l:data
43 let l:should_lint = ale#Var(a:buffer, 'fix_on_save')
44 \ && ale#Var(a:buffer, 'lint_on_save')
46 let l:should_lint = l:data.changes_made
49 silent doautocmd <nomodeline> User ALEFixPost
51 " If ALE linting is enabled, check for problems with the file again after
55 \&& !ale#events#QuitRecently(a:buffer)
56 call ale#Queue(0, l:data.should_save ? 'lint_file' : '')
60 function! ale#fix#ApplyFixes(buffer, output) abort
61 let l:data = g:ale_fix_buffer_data[a:buffer]
62 let l:data.output = a:output
63 let l:data.changes_made = l:data.lines_before !=# l:data.output " no-custom-checks
66 call ale#command#RemoveManagedFiles(a:buffer)
68 if !bufexists(a:buffer)
69 " Remove the buffer data when it doesn't exist.
70 call remove(g:ale_fix_buffer_data, a:buffer)
73 if l:data.changes_made && bufexists(a:buffer)
74 let l:lines = getbufline(a:buffer, 1, '$')
76 if l:data.lines_before != l:lines
77 call remove(g:ale_fix_buffer_data, a:buffer)
79 if !l:data.ignore_file_changed_errors
81 echoerr 'The file was changed before fixing finished'
88 " We can only change the lines of a buffer which is currently open,
89 " so try and apply the fixes to the current buffer.
90 call ale#fix#ApplyQueuedFixes(a:buffer)
93 function! s:HandleExit(job_info, buffer, job_output, data) abort
94 let l:buffer_info = get(g:ale_fix_buffer_data, a:buffer, {})
96 if empty(l:buffer_info)
100 if a:job_info.read_temporary_file
101 let l:output = !empty(a:data.temporary_file)
102 \ ? readfile(a:data.temporary_file)
105 let l:output = a:job_output
108 let l:ProcessWith = get(a:job_info, 'process_with', v:null)
110 " Post-process the output with a function if we have one.
111 if l:ProcessWith isnot v:null
112 let l:output = call(l:ProcessWith, [a:buffer, l:output])
115 " Use the output of the job for changing the file if it isn't empty,
116 " otherwise skip this job and use the input from before.
118 " We'll use the input from before for chained commands.
119 if !empty(split(join(l:output)))
120 let l:input = l:output
122 let l:input = a:job_info.input
126 \ 'buffer': a:buffer,
128 \ 'callback_list': a:job_info.callback_list,
129 \ 'callback_index': a:job_info.callback_index + 1,
133 function! s:RunJob(result, options) abort
134 if ale#command#IsDeferred(a:result)
135 let a:result.result_callback = {x -> s:RunJob(x, a:options)}
140 let l:buffer = a:options.buffer
141 let l:input = a:options.input
142 let l:fixer_name = a:options.fixer_name
144 if a:result is 0 || type(a:result) is v:t_list
145 if type(a:result) is v:t_list
146 let l:input = a:result
150 \ 'buffer': l:buffer,
152 \ 'callback_index': a:options.callback_index + 1,
153 \ 'callback_list': a:options.callback_list,
159 let l:command = get(a:result, 'command', '')
162 " If the command is empty, skip to the next item.
164 \ 'buffer': l:buffer,
166 \ 'callback_index': a:options.callback_index,
167 \ 'callback_list': a:options.callback_list,
173 let l:read_temporary_file = get(a:result, 'read_temporary_file', 0)
174 let l:read_buffer = get(a:result, 'read_buffer', 1)
175 let l:output_stream = get(a:result, 'output_stream', 'stdout')
176 let l:cwd = get(a:result, 'cwd', v:null)
178 if l:read_temporary_file
179 let l:output_stream = 'none'
182 let l:Callback = function('s:HandleExit', [{
184 \ 'callback_index': a:options.callback_index,
185 \ 'callback_list': a:options.callback_list,
186 \ 'process_with': get(a:result, 'process_with', v:null),
187 \ 'read_temporary_file': l:read_temporary_file,
189 let l:run_result = ale#command#Run(l:buffer, l:command, l:Callback, {
190 \ 'output_stream': l:output_stream,
192 \ 'read_buffer': l:read_buffer,
196 \ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:fixer_name),
199 if empty(l:run_result)
201 \ 'buffer': l:buffer,
203 \ 'callback_index': a:options.callback_index + 1,
204 \ 'callback_list': a:options.callback_list,
209 function! s:RunFixer(options) abort
210 let l:buffer = a:options.buffer
211 let l:input = a:options.input
212 let l:index = a:options.callback_index
214 if len(a:options.callback_list) <= l:index
215 call ale#fix#ApplyFixes(l:buffer, l:input)
220 let [l:fixer_name, l:Function] = a:options.callback_list[l:index]
222 " Record new jobs started as fixer jobs.
223 call setbufvar(l:buffer, 'ale_job_type', 'fixer')
225 " Regular fixer commands accept (buffer, [input])
226 let l:result = ale#util#FunctionArgCount(l:Function) == 1
227 \ ? call(l:Function, [l:buffer])
228 \ : call(l:Function, [l:buffer, copy(l:input)])
230 call s:RunJob(l:result, {
231 \ 'buffer': l:buffer,
233 \ 'callback_list': a:options.callback_list,
234 \ 'callback_index': l:index,
235 \ 'fixer_name': l:fixer_name,
239 function! s:AddSubCallbacks(full_list, callbacks) abort
240 if type(a:callbacks) is v:t_string
241 call add(a:full_list, a:callbacks)
242 elseif type(a:callbacks) is v:t_list
243 call extend(a:full_list, a:callbacks)
251 function! s:IgnoreFixers(callback_list, filetype, config) abort
252 if type(a:config) is v:t_list
253 let l:ignore_list = a:config
255 let l:ignore_list = []
257 for l:part in split(a:filetype , '\.')
258 call extend(l:ignore_list, get(a:config, l:part, []))
262 call filter(a:callback_list, 'index(l:ignore_list, v:val) < 0')
265 function! s:GetCallbacks(buffer, fixing_flag, fixers) abort
267 let l:callback_list = a:fixers
268 elseif type(get(b:, 'ale_fixers')) is v:t_list
269 " Lists can be used for buffer-local variables only
270 let l:callback_list = b:ale_fixers
272 " buffer and global options can use dictionaries mapping filetypes to
274 let l:fixers = ale#Var(a:buffer, 'fixers')
275 let l:callback_list = []
278 for l:sub_type in split(&filetype, '\.')
279 if s:AddSubCallbacks(l:callback_list, get(l:fixers, l:sub_type))
284 " If we couldn't find fixers for a filetype, default to '*' fixers.
286 call s:AddSubCallbacks(l:callback_list, get(l:fixers, '*'))
290 if a:fixing_flag is# 'save_file'
291 let l:config = ale#Var(a:buffer, 'fix_on_save_ignore')
294 call s:IgnoreFixers(l:callback_list, &filetype, l:config)
298 let l:corrected_list = []
300 " Variables with capital characters are needed, or Vim will complain about
302 for l:Item in l:callback_list
303 " Try to capture the names of registered fixer names, so we can use
304 " them for filename mapping or other purposes later.
305 let l:fixer_name = v:null
307 if type(l:Item) is v:t_string
308 let l:Func = ale#fix#registry#GetFunc(l:Item)
311 let l:fixer_name = l:Item
317 call add(l:corrected_list, [
319 \ ale#util#GetFunction(l:Item)
322 " Rethrow exceptions for failing to get a function so we can print
323 " a friendly message about it.
324 throw 'BADNAME ' . v:exception
328 return l:corrected_list
331 function! ale#fix#InitBufferData(buffer, fixing_flag) abort
332 " The 'done' flag tells the function for applying changes when fixing
334 let g:ale_fix_buffer_data[a:buffer] = {
335 \ 'lines_before': getbufline(a:buffer, 1, '$'),
337 \ 'should_save': a:fixing_flag is# 'save_file',
338 \ 'ignore_file_changed_errors': a:fixing_flag is# '!',
339 \ 'temporary_directory_list': [],
343 " Accepts an optional argument for what to do when fixing.
345 " Returns 0 if no fixes can be applied, and 1 if fixing can be done.
346 function! ale#fix#Fix(buffer, fixing_flag, ...) abort
347 if a:fixing_flag isnot# ''
348 \&& a:fixing_flag isnot# '!'
349 \&& a:fixing_flag isnot# 'save_file'
350 throw "fixing_flag must be '', '!', or 'save_file'"
354 let l:callback_list = s:GetCallbacks(a:buffer, a:fixing_flag, a:000)
355 catch /E700\|BADNAME/
356 if a:fixing_flag isnot# '!'
357 let l:function_name = join(split(split(v:exception, ':')[3]))
358 let l:echo_message = printf(
359 \ 'There is no fixer named `%s`. Check :ALEFixSuggest',
369 if empty(l:callback_list)
370 if a:fixing_flag is# ''
372 echom 'No fixers have been defined. Try :ALEFixSuggest'
378 call ale#command#StopJobs(a:buffer, 'fixer')
379 " Clean up any files we might have left behind from a previous run.
380 call ale#command#RemoveManagedFiles(a:buffer)
381 call ale#fix#InitBufferData(a:buffer, a:fixing_flag)
383 silent doautocmd <nomodeline> User ALEFixPre
386 \ 'buffer': a:buffer,
387 \ 'input': g:ale_fix_buffer_data[a:buffer].lines_before,
388 \ 'callback_index': 0,
389 \ 'callback_list': l:callback_list,
395 " Set up an autocmd command to try and apply buffer fixes when available.
396 augroup ALEBufferFixGroup
398 autocmd BufEnter * call ale#fix#ApplyQueuedFixes(str2nr(expand('<abuf>')))