summaryrefslogtreecommitdiff
path: root/autoload
diff options
context:
space:
mode:
authorw0rp <devw0rp@gmail.com>2017-05-20 19:02:56 +0100
committerw0rp <devw0rp@gmail.com>2017-05-20 19:02:56 +0100
commitbf8bf0668113a1c5a378f05050722967f88a273f (patch)
treece8125f33a5a2e082d5e8d68e8234445c7ca8331 /autoload
parent0d797c203f22e593a6d19d127a8d1f4f78d3d106 (diff)
parent74d879952cfa3a27b21869bdbfef909c793178bb (diff)
downloadale-bf8bf0668113a1c5a378f05050722967f88a273f.zip
Merge branch 'error-fixing'
Diffstat (limited to 'autoload')
-rw-r--r--autoload/ale/engine.vim3
-rw-r--r--autoload/ale/fix.vim308
-rw-r--r--autoload/ale/fix/generic.vim12
-rw-r--r--autoload/ale/fix/registry.vim134
-rw-r--r--autoload/ale/handlers/eslint.vim43
-rw-r--r--autoload/ale/handlers/python.vim28
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