summaryrefslogtreecommitdiff
path: root/autoload/ale/fix.vim
blob: d9820847583475ef9b9781857653bb5e43ac56be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
" 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
                " no-custom-checks
                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,
            \)
            " no-custom-checks
            echom l:echo_message
        endif

        return 0
    endtry

    if empty(l:callback_list)
        if a:fixing_flag is# ''
            " no-custom-checks
            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