diff options
author | w0rp <devw0rp@gmail.com> | 2017-05-20 19:02:56 +0100 |
---|---|---|
committer | w0rp <devw0rp@gmail.com> | 2017-05-20 19:02:56 +0100 |
commit | bf8bf0668113a1c5a378f05050722967f88a273f (patch) | |
tree | ce8125f33a5a2e082d5e8d68e8234445c7ca8331 /autoload | |
parent | 0d797c203f22e593a6d19d127a8d1f4f78d3d106 (diff) | |
parent | 74d879952cfa3a27b21869bdbfef909c793178bb (diff) | |
download | ale-bf8bf0668113a1c5a378f05050722967f88a273f.zip |
Merge branch 'error-fixing'
Diffstat (limited to 'autoload')
-rw-r--r-- | autoload/ale/engine.vim | 3 | ||||
-rw-r--r-- | autoload/ale/fix.vim | 308 | ||||
-rw-r--r-- | autoload/ale/fix/generic.vim | 12 | ||||
-rw-r--r-- | autoload/ale/fix/registry.vim | 134 | ||||
-rw-r--r-- | autoload/ale/handlers/eslint.vim | 43 | ||||
-rw-r--r-- | autoload/ale/handlers/python.vim | 28 |
6 files changed, 526 insertions, 2 deletions
diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index af074c00..e13562a0 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -405,8 +405,7 @@ function! s:RunJob(options) abort \ : l:command \) - " TODO, get the exit system of the shell call and pass it on here. - call l:job_options.exit_cb(l:job_id, 0) + call l:job_options.exit_cb(l:job_id, v:shell_error) endif endfunction diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim new file mode 100644 index 00000000..e329693d --- /dev/null +++ b/autoload/ale/fix.vim @@ -0,0 +1,308 @@ +" FIXME: Switch to using the global buffer data dictionary instead. +" Cleanup will work better if there isn't a second Dictionary we have to work +" with. +let s:buffer_data = {} +let s:job_info_map = {} + +function! s:GatherOutput(job_id, line) abort + if has_key(s:job_info_map, a:job_id) + call add(s:job_info_map[a:job_id].output, a:line) + endif +endfunction + +function! ale#fix#ApplyQueuedFixes() abort + let l:buffer = bufnr('') + let l:data = get(s:buffer_data, l:buffer, {'done': 0}) + + if !l:data.done + return + endif + + call remove(s:buffer_data, l:buffer) + let l:lines = getbufline(l:buffer, 1, '$') + + if l:data.lines_before != l:lines + echoerr 'The file was changed before fixing finished' + return + endif + + if l:data.lines_before == l:data.output + " Don't modify the buffer if nothing has changed. + return + endif + + call setline(1, l:data.output) + + let l:start_line = len(l:data.output) + 1 + let l:end_line = len(l:lines) + + if l:end_line >= l:start_line + let l:save = winsaveview() + silent execute l:start_line . ',' . l:end_line . 'd' + call winrestview(l:save) + endif + + " If ALE linting is enabled, check for problems with the file again after + " fixing problems. + if g:ale_enabled + call ale#Queue(g:ale_lint_delay) + endif +endfunction + +function! s:ApplyFixes(buffer, output) abort + call ale#fix#RemoveManagedFiles(a:buffer) + + let s:buffer_data[a:buffer].output = a:output + let s:buffer_data[a:buffer].done = 1 + + " 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() +endfunction + +function! s:HandleExit(job_id, exit_code) abort + if !has_key(s:job_info_map, a:job_id) + return + endif + + let l:job_info = remove(s:job_info_map, a:job_id) + + if has_key(l:job_info, 'file_to_read') + let l:job_info.output = readfile(l:job_info.file_to_read) + endif + + call s:RunFixer({ + \ 'buffer': l:job_info.buffer, + \ 'input': l:job_info.output, + \ 'callback_list': l:job_info.callback_list, + \ 'callback_index': l:job_info.callback_index + 1, + \}) +endfunction + +function! ale#fix#ManageDirectory(buffer, directory) abort + call add(s:buffer_data[a:buffer].temporary_directory_list, a:directory) +endfunction + +function! ale#fix#RemoveManagedFiles(buffer) abort + if !has_key(s:buffer_data, a:buffer) + return + endif + + " We can't delete anything in a sandbox, so wait until we escape from + " it to delete temporary files and directories. + if ale#util#InSandbox() + return + endif + + " Delete directories like `rm -rf`. + " Directories are handled differently from files, so paths that are + " intended to be single files can be set up for automatic deletion without + " accidentally deleting entire directories. + for l:directory in s:buffer_data[a:buffer].temporary_directory_list + call delete(l:directory, 'rf') + endfor + + let s:buffer_data[a:buffer].temporary_directory_list = [] +endfunction + +function! s:CreateTemporaryFileForJob(buffer, temporary_file, input) abort + if empty(a:temporary_file) + " There is no file, so we didn't create anything. + return 0 + endif + + let l:temporary_directory = fnamemodify(a:temporary_file, ':h') + " Create the temporary directory for the file, unreadable by 'other' + " users. + call mkdir(l:temporary_directory, '', 0750) + " Automatically delete the directory later. + call ale#fix#ManageDirectory(a:buffer, l:temporary_directory) + " Write the buffer out to a file. + call writefile(a:input, a:temporary_file) + + return 1 +endfunction + +function! s:RunJob(options) abort + let l:buffer = a:options.buffer + let l:command = a:options.command + let l:input = a:options.input + let l:output_stream = a:options.output_stream + let l:read_temporary_file = a:options.read_temporary_file + + let [l:temporary_file, l:command] = ale#command#FormatCommand(l:buffer, l:command, 1) + call s:CreateTemporaryFileForJob(l:buffer, l:temporary_file, l:input) + + let l:command = ale#job#PrepareCommand(l:command) + let l:job_options = { + \ 'mode': 'nl', + \ 'exit_cb': function('s:HandleExit'), + \} + + let l:job_info = { + \ 'buffer': l:buffer, + \ 'output': [], + \ 'callback_list': a:options.callback_list, + \ 'callback_index': a:options.callback_index, + \} + + if l:read_temporary_file + " TODO: Check that a temporary file is set here. + let l:job_info.file_to_read = l:temporary_file + elseif l:output_stream ==# 'stderr' + let l:job_options.err_cb = function('s:GatherOutput') + elseif l:output_stream ==# 'both' + let l:job_options.out_cb = function('s:GatherOutput') + let l:job_options.err_cb = function('s:GatherOutput') + else + let l:job_options.out_cb = function('s:GatherOutput') + endif + + if get(g:, 'ale_emulate_job_failure') == 1 + let l:job_id = 0 + elseif get(g:, 'ale_run_synchronously') == 1 + " Find a unique Job value to use, which will be the same as the ID for + " running commands synchronously. This is only for test code. + let l:job_id = len(s:job_info_map) + 1 + + while has_key(s:job_info_map, l:job_id) + let l:job_id += 1 + endwhile + else + let l:job_id = ale#job#Start(l:command, l:job_options) + endif + + if l:job_id == 0 + return 0 + endif + + let s:job_info_map[l:job_id] = l:job_info + + if get(g:, 'ale_run_synchronously') == 1 + " Run a command synchronously if this test option is set. + let l:output = systemlist( + \ type(l:command) == type([]) + \ ? join(l:command[0:1]) . ' ' . ale#Escape(l:command[2]) + \ : l:command + \) + + if !l:read_temporary_file + let s:job_info_map[l:job_id].output = l:output + endif + + call l:job_options.exit_cb(l:job_id, v:shell_error) + endif + + return 1 +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 + + while len(a:options.callback_list) > l:index + let l:result = call(a:options.callback_list[l:index], [l:buffer, copy(l:input)]) + + if type(l:result) == type(0) && l:result == 0 + " When `0` is returned, skip this item. + let l:index += 1 + elseif type(l:result) == type([]) + let l:input = l:result + let l:index += 1 + else + let l:job_ran = s:RunJob({ + \ 'buffer': l:buffer, + \ 'command': l:result.command, + \ 'input': l:input, + \ 'output_stream': get(l:result, 'output_stream', 'stdout'), + \ 'read_temporary_file': get(l:result, 'read_temporary_file', 0), + \ 'callback_list': a:options.callback_list, + \ 'callback_index': l:index, + \}) + + if !l:job_ran + " The job failed to run, so skip to the next item. + let l:index += 1 + else + " Stop here, we will handle exit later on. + return + endif + endif + endwhile + + call s:ApplyFixes(l:buffer, l:input) +endfunction + +function! s:GetCallbacks() abort + let l:fixers = ale#Var(bufnr(''), 'fixers') + let l:callback_list = [] + + for l:sub_type in split(&filetype, '\.') + let l:sub_type_callacks = get(l:fixers, l:sub_type, []) + + if type(l:sub_type_callacks) == type('') + call add(l:callback_list, l:sub_type_callacks) + else + call extend(l:callback_list, l:sub_type_callacks) + endif + endfor + + if empty(l:callback_list) + echoerr 'No fixers have been defined. Try :ALEFixSuggest' + return [] + 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 + if type(l:Item) == type('') + let l:Func = ale#fix#registry#GetFunc(l:Item) + + if !empty(l:Func) + let l:Item = l:Func + endif + endif + + call add(l:corrected_list, ale#util#GetFunction(l:Item)) + endfor + + return l:corrected_list +endfunction + +function! ale#fix#Fix() abort + let l:callback_list = s:GetCallbacks() + + if empty(l:callback_list) + return + endif + + let l:buffer = bufnr('') + let l:input = getbufline(l:buffer, 1, '$') + + " Clean up any files we might have left behind from a previous run. + call ale#fix#RemoveManagedFiles(l:buffer) + + " The 'done' flag tells the function for applying changes when fixing + " is complete. + let s:buffer_data[l:buffer] = { + \ 'lines_before': l:input, + \ 'done': 0, + \ 'temporary_directory_list': [], + \} + + call s:RunFixer({ + \ 'buffer': l:buffer, + \ 'input': l:input, + \ 'callback_index': 0, + \ 'callback_list': l:callback_list, + \}) +endfunction + +" Set up an autocmd command to try and apply buffer fixes when available. +augroup ALEBufferFixGroup + autocmd! + autocmd BufEnter * call ale#fix#ApplyQueuedFixes() +augroup END diff --git a/autoload/ale/fix/generic.vim b/autoload/ale/fix/generic.vim new file mode 100644 index 00000000..5c5b2007 --- /dev/null +++ b/autoload/ale/fix/generic.vim @@ -0,0 +1,12 @@ +" Author: w0rp <devw0rp@gmail.com> +" Description: Generic functions for fixing files with. + +function! ale#fix#generic#RemoveTrailingBlankLines(buffer, lines) abort + let l:end_index = len(a:lines) - 1 + + while l:end_index > 0 && empty(a:lines[l:end_index]) + let l:end_index -= 1 + endwhile + + return a:lines[:l:end_index] +endfunction diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim new file mode 100644 index 00000000..b85c5d76 --- /dev/null +++ b/autoload/ale/fix/registry.vim @@ -0,0 +1,134 @@ +" Author: w0rp <devw0rp@gmail.com> +" Description: A registry of functions for fixing things. + +let s:default_registry = { +\ 'autopep8': { +\ 'function': 'ale#handlers#python#AutoPEP8', +\ 'suggested_filetypes': ['python'], +\ 'description': 'Fix PEP8 issues with autopep8.', +\ }, +\ 'eslint': { +\ 'function': 'ale#handlers#eslint#Fix', +\ 'suggested_filetypes': ['javascript'], +\ 'description': 'Apply eslint --fix to a file.', +\ }, +\ 'isort': { +\ 'function': 'ale#handlers#python#ISort', +\ 'suggested_filetypes': ['python'], +\ 'description': 'Sort Python imports with isort.', +\ }, +\ 'remove_trailing_lines': { +\ 'function': 'ale#fix#generic#RemoveTrailingBlankLines', +\ 'suggested_filetypes': [], +\ 'description': 'Remove all blank lines at the end of a file.', +\ }, +\ 'yapf': { +\ 'function': 'ale#handlers#python#YAPF', +\ 'suggested_filetypes': ['python'], +\ 'description': 'Fix Python files with yapf.', +\ }, +\} + +" Reset the function registry to the default entries. +function! ale#fix#registry#ResetToDefaults() abort + let s:entries = deepcopy(s:default_registry) +endfunction + +" Set up entries now. +call ale#fix#registry#ResetToDefaults() + +" Remove everything from the registry, useful for tests. +function! ale#fix#registry#Clear() abort + let s:entries = {} +endfunction + +" Add a function for fixing problems to the registry. +function! ale#fix#registry#Add(name, func, filetypes, desc) abort + if type(a:name) != type('') + throw '''name'' must be a String' + endif + + if type(a:func) != type('') + throw '''func'' must be a String' + endif + + if type(a:filetypes) != type([]) + throw '''filetypes'' must be a List' + endif + + for l:type in a:filetypes + if type(l:type) != type('') + throw 'Each entry of ''filetypes'' must be a String' + endif + endfor + + if type(a:desc) != type('') + throw '''desc'' must be a String' + endif + + let s:entries[a:name] = { + \ 'function': a:func, + \ 'suggested_filetypes': a:filetypes, + \ 'description': a:desc, + \} +endfunction + +" Get a function from the registry by its short name. +function! ale#fix#registry#GetFunc(name) abort + return get(s:entries, a:name, {'function': ''}).function +endfunction + +function! s:ShouldSuggestForType(suggested_filetypes, type_list) abort + for l:type in a:type_list + if index(a:suggested_filetypes, l:type) >= 0 + return 1 + endif + endfor + + return 0 +endfunction + +" Suggest functions to use from the registry. +function! ale#fix#registry#Suggest(filetype) abort + let l:type_list = split(a:filetype, '\.') + let l:first_for_filetype = 1 + let l:first_generic = 1 + + for l:key in sort(keys(s:entries)) + let l:suggested_filetypes = s:entries[l:key].suggested_filetypes + + if s:ShouldSuggestForType(l:suggested_filetypes, l:type_list) + if l:first_for_filetype + let l:first_for_filetype = 0 + echom 'Try the following fixers appropriate for the filetype:' + echom '' + endif + + echom printf('%s - %s', string(l:key), s:entries[l:key].description) + endif + endfor + + + for l:key in sort(keys(s:entries)) + if empty(s:entries[l:key].suggested_filetypes) + if l:first_generic + if !l:first_for_filetype + echom '' + endif + + let l:first_generic = 0 + echom 'Try the following generic fixers:' + echom '' + endif + + echom printf('%s - %s', string(l:key), s:entries[l:key].description) + endif + endfor + + if l:first_for_filetype && l:first_generic + echom 'There is nothing in the registry to suggest.' + else + echom '' + echom 'See :help ale-fix-configuration' + endif +endfunction diff --git a/autoload/ale/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim new file mode 100644 index 00000000..a7e8ef42 --- /dev/null +++ b/autoload/ale/handlers/eslint.vim @@ -0,0 +1,43 @@ +" Author: w0rp <devw0rp@gmail.com> +" Description: eslint functions for handling and fixing errors. + +let g:ale_javascript_eslint_executable = +\ get(g:, 'ale_javascript_eslint_executable', 'eslint') + +function! ale#handlers#eslint#GetExecutable(buffer) abort + if ale#Var(a:buffer, 'javascript_eslint_use_global') + return ale#Var(a:buffer, 'javascript_eslint_executable') + endif + + " Look for the kinds of paths that create-react-app generates first. + let l:executable = ale#path#ResolveLocalPath( + \ a:buffer, + \ 'node_modules/eslint/bin/eslint.js', + \ '' + \) + + if !empty(l:executable) + return l:executable + endif + + return ale#path#ResolveLocalPath( + \ a:buffer, + \ 'node_modules/.bin/eslint', + \ ale#Var(a:buffer, 'javascript_eslint_executable') + \) +endfunction + +function! ale#handlers#eslint#Fix(buffer, lines) abort + let l:config = ale#path#FindNearestFile(a:buffer, '.eslintrc.js') + + if empty(l:config) + return 0 + endif + + return { + \ 'command': ale#Escape(ale#handlers#eslint#GetExecutable(a:buffer)) + \ . ' --config ' . ale#Escape(l:config) + \ . ' --fix %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/handlers/python.vim b/autoload/ale/handlers/python.vim index 85e2f203..5e9ddecd 100644 --- a/autoload/ale/handlers/python.vim +++ b/autoload/ale/handlers/python.vim @@ -35,3 +35,31 @@ function! ale#handlers#python#HandlePEP8Format(buffer, lines) abort return l:output endfunction + +function! ale#handlers#python#AutoPEP8(buffer, lines) abort + return { + \ 'command': 'autopep8 -' + \} +endfunction + +function! ale#handlers#python#ISort(buffer, lines) abort + let l:config = ale#path#FindNearestFile(a:buffer, '.isort.cfg') + let l:config_options = !empty(l:config) + \ ? ' --settings-path ' . ale#Escape(l:config) + \ : '' + + return { + \ 'command': 'isort' . l:config_options . ' -', + \} +endfunction + +function! ale#handlers#python#YAPF(buffer, lines) abort + let l:config = ale#path#FindNearestFile(a:buffer, '.style.yapf') + let l:config_options = !empty(l:config) + \ ? ' --style ' . ale#Escape(l:config) + \ : '' + + return { + \ 'command': 'yapf --no-local-style' . l:config_options, + \} +endfunction |