]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/fix.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / fix.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Functions for fixing code with programs, or other means.
3
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', {})
6
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})
11
12     if !l:data.done || (!ale#util#HasBuflineApi() && a:buffer isnot bufnr(''))
13         return
14     endif
15
16     call remove(g:ale_fix_buffer_data, a:buffer)
17
18     try
19         if l:data.changes_made
20             let l:new_lines = ale#util#SetBufferContents(a:buffer, l:data.output)
21
22             if l:data.should_save
23                 if a:buffer is bufnr('')
24                     if empty(&buftype)
25                         noautocmd :w!
26                     else
27                         set nomodified
28                     endif
29                 else
30                     call writefile(l:new_lines, expand('#' . a:buffer . ':p')) " no-custom-checks
31                     call setbufvar(a:buffer, '&modified', 0)
32                 endif
33             endif
34         endif
35     catch /E21\|E5555/
36         " If we cannot modify the buffer now, try again later.
37         let g:ale_fix_buffer_data[a:buffer] = l:data
38
39         return
40     endtry
41
42     if l:data.should_save
43         let l:should_lint = ale#Var(a:buffer, 'fix_on_save')
44         \   && ale#Var(a:buffer, 'lint_on_save')
45     else
46         let l:should_lint = l:data.changes_made
47     endif
48
49     silent doautocmd <nomodeline> User ALEFixPost
50
51     " If ALE linting is enabled, check for problems with the file again after
52     " fixing problems.
53     if g:ale_enabled
54     \&& l:should_lint
55     \&& !ale#events#QuitRecently(a:buffer)
56         call ale#Queue(0, l:data.should_save ? 'lint_file' : '')
57     endif
58 endfunction
59
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
64     let l:data.done = 1
65
66     call ale#command#RemoveManagedFiles(a:buffer)
67
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)
71     endif
72
73     if l:data.changes_made && bufexists(a:buffer)
74         let l:lines = getbufline(a:buffer, 1, '$')
75
76         if l:data.lines_before != l:lines
77             call remove(g:ale_fix_buffer_data, a:buffer)
78
79             if !l:data.ignore_file_changed_errors
80                 " no-custom-checks
81                 echoerr 'The file was changed before fixing finished'
82             endif
83
84             return
85         endif
86     endif
87
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)
91 endfunction
92
93 function! s:HandleExit(job_info, buffer, job_output, data) abort
94     let l:buffer_info = get(g:ale_fix_buffer_data, a:buffer, {})
95
96     if empty(l:buffer_info)
97         return
98     endif
99
100     if a:job_info.read_temporary_file
101         let l:output = !empty(a:data.temporary_file)
102         \   ?  readfile(a:data.temporary_file)
103         \   : []
104     else
105         let l:output = a:job_output
106     endif
107
108     let l:ProcessWith = get(a:job_info, 'process_with', v:null)
109
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])
113     endif
114
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.
117     "
118     " We'll use the input from before for chained commands.
119     if !empty(split(join(l:output)))
120         let l:input = l:output
121     else
122         let l:input = a:job_info.input
123     endif
124
125     call s:RunFixer({
126     \   'buffer': a:buffer,
127     \   'input': l:input,
128     \   'callback_list': a:job_info.callback_list,
129     \   'callback_index': a:job_info.callback_index + 1,
130     \})
131 endfunction
132
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)}
136
137         return
138     endif
139
140     let l:buffer = a:options.buffer
141     let l:input = a:options.input
142     let l:fixer_name = a:options.fixer_name
143
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
147         endif
148
149         call s:RunFixer({
150         \   'buffer': l:buffer,
151         \   'input': l:input,
152         \   'callback_index': a:options.callback_index + 1,
153         \   'callback_list': a:options.callback_list,
154         \})
155
156         return
157     endif
158
159     let l:command = get(a:result, 'command', '')
160
161     if empty(l:command)
162         " If the command is empty, skip to the next item.
163         call s:RunFixer({
164         \   'buffer': l:buffer,
165         \   'input': l:input,
166         \   'callback_index': a:options.callback_index,
167         \   'callback_list': a:options.callback_list,
168         \})
169
170         return
171     endif
172
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)
177
178     if l:read_temporary_file
179         let l:output_stream = 'none'
180     endif
181
182     let l:Callback = function('s:HandleExit', [{
183     \   'input': l:input,
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,
188     \}])
189     let l:run_result = ale#command#Run(l:buffer, l:command, l:Callback, {
190     \   'output_stream': l:output_stream,
191     \   'executable': '',
192     \   'read_buffer': l:read_buffer,
193     \   'input': l:input,
194     \   'log_output': 0,
195     \   'cwd': l:cwd,
196     \   'filename_mappings': ale#GetFilenameMappings(l:buffer, l:fixer_name),
197     \})
198
199     if empty(l:run_result)
200         call s:RunFixer({
201         \   'buffer': l:buffer,
202         \   'input': l:input,
203         \   'callback_index': a:options.callback_index + 1,
204         \   'callback_list': a:options.callback_list,
205         \})
206     endif
207 endfunction
208
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
213
214     if len(a:options.callback_list) <= l:index
215         call ale#fix#ApplyFixes(l:buffer, l:input)
216
217         return
218     endif
219
220     let [l:fixer_name, l:Function] = a:options.callback_list[l:index]
221
222     " Record new jobs started as fixer jobs.
223     call setbufvar(l:buffer, 'ale_job_type', 'fixer')
224
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)])
229
230     call s:RunJob(l:result, {
231     \   'buffer': l:buffer,
232     \   'input': l:input,
233     \   'callback_list': a:options.callback_list,
234     \   'callback_index': l:index,
235     \   'fixer_name': l:fixer_name,
236     \})
237 endfunction
238
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)
244     else
245         return 0
246     endif
247
248     return 1
249 endfunction
250
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
254     else
255         let l:ignore_list = []
256
257         for l:part in split(a:filetype , '\.')
258             call extend(l:ignore_list, get(a:config, l:part, []))
259         endfor
260     endif
261
262     call filter(a:callback_list, 'index(l:ignore_list, v:val) < 0')
263 endfunction
264
265 function! s:GetCallbacks(buffer, fixing_flag, fixers) abort
266     if len(a:fixers)
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
271     else
272         " buffer and global options can use dictionaries mapping filetypes to
273         " callbacks to run.
274         let l:fixers = ale#Var(a:buffer, 'fixers')
275         let l:callback_list = []
276         let l:matched = 0
277
278         for l:sub_type in split(&filetype, '\.')
279             if s:AddSubCallbacks(l:callback_list, get(l:fixers, l:sub_type))
280                 let l:matched = 1
281             endif
282         endfor
283
284         " If we couldn't find fixers for a filetype, default to '*' fixers.
285         if !l:matched
286             call s:AddSubCallbacks(l:callback_list, get(l:fixers, '*'))
287         endif
288     endif
289
290     if a:fixing_flag is# 'save_file'
291         let l:config = ale#Var(a:buffer, 'fix_on_save_ignore')
292
293         if !empty(l:config)
294             call s:IgnoreFixers(l:callback_list, &filetype, l:config)
295         endif
296     endif
297
298     let l:corrected_list = []
299
300     " Variables with capital characters are needed, or Vim will complain about
301     " funcref variables.
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
306
307         if type(l:Item) is v:t_string
308             let l:Func = ale#fix#registry#GetFunc(l:Item)
309
310             if !empty(l:Func)
311                 let l:fixer_name = l:Item
312                 let l:Item = l:Func
313             endif
314         endif
315
316         try
317             call add(l:corrected_list, [
318             \   l:fixer_name,
319             \   ale#util#GetFunction(l:Item)
320             \])
321         catch /E475/
322             " Rethrow exceptions for failing to get a function so we can print
323             " a friendly message about it.
324             throw 'BADNAME ' . v:exception
325         endtry
326     endfor
327
328     return l:corrected_list
329 endfunction
330
331 function! ale#fix#InitBufferData(buffer, fixing_flag) abort
332     " The 'done' flag tells the function for applying changes when fixing
333     " is complete.
334     let g:ale_fix_buffer_data[a:buffer] = {
335     \   'lines_before': getbufline(a:buffer, 1, '$'),
336     \   'done': 0,
337     \   'should_save': a:fixing_flag is# 'save_file',
338     \   'ignore_file_changed_errors': a:fixing_flag is# '!',
339     \   'temporary_directory_list': [],
340     \}
341 endfunction
342
343 " Accepts an optional argument for what to do when fixing.
344 "
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'"
351     endif
352
353     try
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',
360             \   l:function_name,
361             \)
362             " no-custom-checks
363             echom l:echo_message
364         endif
365
366         return 0
367     endtry
368
369     if empty(l:callback_list)
370         if a:fixing_flag is# ''
371             " no-custom-checks
372             echom 'No fixers have been defined. Try :ALEFixSuggest'
373         endif
374
375         return 0
376     endif
377
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)
382
383     silent doautocmd <nomodeline> User ALEFixPre
384
385     call s:RunFixer({
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,
390     \})
391
392     return 1
393 endfunction
394
395 " Set up an autocmd command to try and apply buffer fixes when available.
396 augroup ALEBufferFixGroup
397     autocmd!
398     autocmd BufEnter * call ale#fix#ApplyQueuedFixes(str2nr(expand('<abuf>')))
399 augroup END