Merge branch 'error-fixing'

This commit is contained in:
w0rp 2017-05-20 19:02:56 +01:00
commit bf8bf06681
14 changed files with 905 additions and 46 deletions

View File

@ -15,6 +15,9 @@ back to a filesystem.
In other words, this plugin allows you to lint while you type. In other words, this plugin allows you to lint while you type.
ALE also supports fixing problems with files by running commands in the
background with a command `ALEFix`.
## Table of Contents ## Table of Contents
1. [Supported Languages and Tools](#supported-languages) 1. [Supported Languages and Tools](#supported-languages)
@ -138,6 +141,9 @@ documented in [the Vim help file](doc/ale.txt). For more information on the
options ALE offers, consult `:help ale-options` for global options and `:help options ALE offers, consult `:help ale-options` for global options and `:help
ale-linter-options` for options specified to particular linters. ale-linter-options` for options specified to particular linters.
ALE can fix files with the `ALEFix` command. Functions need to be configured
for different filetypes with the `g:ale_fixers` variable. See `:help ale-fix`.
<a name="installation"></a> <a name="installation"></a>
## 3. Installation ## 3. Installation

View File

@ -1,40 +1,14 @@
" Author: w0rp <devw0rp@gmail.com> " Author: w0rp <devw0rp@gmail.com>
" Description: eslint for JavaScript files " Description: eslint for JavaScript files
let g:ale_javascript_eslint_executable =
\ get(g:, 'ale_javascript_eslint_executable', 'eslint')
let g:ale_javascript_eslint_options = let g:ale_javascript_eslint_options =
\ get(g:, 'ale_javascript_eslint_options', '') \ get(g:, 'ale_javascript_eslint_options', '')
let g:ale_javascript_eslint_use_global = let g:ale_javascript_eslint_use_global =
\ get(g:, 'ale_javascript_eslint_use_global', 0) \ get(g:, 'ale_javascript_eslint_use_global', 0)
function! ale_linters#javascript#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_linters#javascript#eslint#GetCommand(buffer) abort function! ale_linters#javascript#eslint#GetCommand(buffer) abort
return ale#Escape(ale_linters#javascript#eslint#GetExecutable(a:buffer)) return ale#handlers#eslint#GetExecutable(a:buffer)
\ . ' ' . ale#Var(a:buffer, 'javascript_eslint_options') \ . ' ' . ale#Var(a:buffer, 'javascript_eslint_options')
\ . ' -f unix --stdin --stdin-filename %s' \ . ' -f unix --stdin --stdin-filename %s'
endfunction endfunction
@ -103,7 +77,7 @@ endfunction
call ale#linter#Define('javascript', { call ale#linter#Define('javascript', {
\ 'name': 'eslint', \ 'name': 'eslint',
\ 'executable_callback': 'ale_linters#javascript#eslint#GetExecutable', \ 'executable_callback': 'ale#handlers#eslint#GetExecutable',
\ 'command_callback': 'ale_linters#javascript#eslint#GetCommand', \ 'command_callback': 'ale_linters#javascript#eslint#GetCommand',
\ 'callback': 'ale_linters#javascript#eslint#Handle', \ 'callback': 'ale_linters#javascript#eslint#Handle',
\}) \})

View File

@ -405,8 +405,7 @@ function! s:RunJob(options) abort
\ : l:command \ : 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, v:shell_error)
call l:job_options.exit_cb(l:job_id, 0)
endif endif
endfunction endfunction

308
autoload/ale/fix.vim Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -35,3 +35,31 @@ function! ale#handlers#python#HandlePEP8Format(buffer, lines) abort
return l:output return l:output
endfunction 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

View File

@ -9,7 +9,8 @@ CONTENTS *ale-contents*
1. Introduction.........................|ale-introduction| 1. Introduction.........................|ale-introduction|
2. Supported Languages & Tools..........|ale-support| 2. Supported Languages & Tools..........|ale-support|
3. Global Options.......................|ale-options| 3. Global Options.......................|ale-options|
4. Linter Options and Recommendations...|ale-linter-options| 4. Fixing Problems......................|ale-fix|
5. Linter Options and Recommendations...|ale-linter-options|
asm...................................|ale-asm-options| asm...................................|ale-asm-options|
gcc.................................|ale-asm-gcc| gcc.................................|ale-asm-gcc|
c.....................................|ale-c-options| c.....................................|ale-c-options|
@ -93,10 +94,10 @@ CONTENTS *ale-contents*
xmllint.............................|ale-xml-xmllint| xmllint.............................|ale-xml-xmllint|
yaml..................................|ale-yaml-options| yaml..................................|ale-yaml-options|
yamllint............................|ale-yaml-yamllint| yamllint............................|ale-yaml-yamllint|
5. Commands/Keybinds....................|ale-commands| 6. Commands/Keybinds....................|ale-commands|
6. API..................................|ale-api| 7. API..................................|ale-api|
7. Special Thanks.......................|ale-special-thanks| 8. Special Thanks.......................|ale-special-thanks|
8. Contact..............................|ale-contact| 9. Contact..............................|ale-contact|
=============================================================================== ===============================================================================
1. Introduction *ale-introduction* 1. Introduction *ale-introduction*
@ -107,7 +108,7 @@ using the |job-control| features available in Vim 8 and NeoVim. For Vim 8,
Vim must be compiled with the |job| and |channel| and |timer| features Vim must be compiled with the |job| and |channel| and |timer| features
as a minimum. as a minimum.
ALE supports the following key features: ALE supports the following key features for linting:
1. Running linters when text is changed. 1. Running linters when text is changed.
2. Running linters when files are opened. 2. Running linters when files are opened.
@ -115,6 +116,10 @@ ALE supports the following key features:
4. Populating the |loclist| with warning and errors. 4. Populating the |loclist| with warning and errors.
5. Setting |signs| with warnings and errors for error markers. 5. Setting |signs| with warnings and errors for error markers.
6. Using |echo| to show error messages when the cursor moves. 6. Using |echo| to show error messages when the cursor moves.
7. Setting syntax highlights for errors.
ALE can fix problems with files with the |ALEFix| command, using the same job
control functionality used for checking for problems.
=============================================================================== ===============================================================================
2. Supported Languages & Tools *ale-support* 2. Supported Languages & Tools *ale-support*
@ -266,6 +271,18 @@ g:ale_enabled *g:ale_enabled*
the |ALEToggle| command, which changes this option. the |ALEToggle| command, which changes this option.
g:ale_fixers *g:ale_fixers*
*b:ale_fixers*
Type: |Dictionary|
Default: `{}`
A mapping from filetypes to |List| values for functions for fixing errors.
See |ale-fix| for more information.
This variable can be overriden with variables in each buffer.
g:ale_history_enabled *g:ale_history_enabled* g:ale_history_enabled *g:ale_history_enabled*
Type: |Number| Type: |Number|
@ -604,7 +621,57 @@ b:ale_warn_about_trailing_whitespace *b:ale_warn_about_trailing_whitespace*
=============================================================================== ===============================================================================
4. Linter Options and Recommendations *ale-linter-options* 4. Fixing Problems *ale-fix*
ALE can fix problems with files with the |ALEFix| command. When |ALEFix| is
run, the variable |g:ale_fixers| will be read for getting a |List| of commands
for filetypes, split on `.`, and the functions named in |g:ale_fixers| will be
executed for fixing the errors.
The values for `g:ale_fixers` can be a list of |String|, |Funcref|, or
|lambda| values. String values must either name a function, or a short name
for a function set in the ALE fixer registry.
Each function for fixing errors must accept two arguments `(buffer, lines)`,
representing the buffer being fixed and the lines to fix. The functions must
return either `0`, for changing nothing, a |List| for new lines to set, or a
|Dictionary| for describing a command to be run in the background.
When a |Dictionary| is returned for an |ALEFix| callback, the following keys
are supported for running the commands.
`command` A |String| for the command to run. This key is required.
When `%t` is included in a command string, a temporary
file will be created, containing the lines from the file
after previous adjustment have been done.
`read_temporary_file` When set to `1`, ALE will read the contents of the
temporary file created for `%t`. This option can be used
for commands which need to modify some file on disk in
order to fix files.
*ale-fix-configuration*
Synchronous functions and asynchronous jobs will be run in a sequence for
fixing files, and can be combined. For example:
>
let g:ale_fixers.javascript = [
\ 'DoSomething',
\ 'eslint',
\ {buffer, lines -> filter(lines, 'v:val !=~ ''^\s*//''')},
\]
ALEFix
<
The above example will call a function called `DoSomething` which could act
upon some lines immediately, then run `eslint` from the ALE registry, and
then call a lambda function which will remove every single line comment
from the file.
===============================================================================
5. Linter Options and Recommendations *ale-linter-options*
Linter options are documented in individual help files. See the table of Linter options are documented in individual help files. See the table of
contents at |ale-contents|. contents at |ale-contents|.
@ -615,7 +682,12 @@ set for `g:ale_python_flake8_executable`.
=============================================================================== ===============================================================================
5. Commands/Keybinds *ale-commands* 6. Commands/Keybinds *ale-commands*
ALEFix *ALEFix*
Fix problems with the current buffer. See |ale-fix| for more information.
ALELint *ALELint* ALELint *ALELint*
@ -676,7 +748,7 @@ ALEDetail *ALEDetail*
A plug mapping `<Plug>(ale_detail)` is defined for this command. A plug mapping `<Plug>(ale_detail)` is defined for this command.
=============================================================================== ===============================================================================
6. API *ale-api* 7. API *ale-api*
ale#Queue(delay, [linting_flag]) *ale#Queue()* ale#Queue(delay, [linting_flag]) *ale#Queue()*
@ -745,6 +817,17 @@ ale#engine#ManageDirectory(buffer, directory) *ale#engine#ManageDirectory()*
files. files.
ale#fix#registry#Add(name, func, filetypes, desc) *ale#fix#registry#Add()*
Given a |String| `name` for a name to add to the registry, a |String| `func`
for a function name, a |List| `filetypes` for a list of filetypes to
set for suggestions, and a |String| `desc` for a short description of
the fixer, register a fixer in the registry.
The `name` can then be used for |g:ale_fixers| in place of the function
name, and suggested for fixing files.
ale#linter#Define(filetype, linter) *ale#linter#Define()* ale#linter#Define(filetype, linter) *ale#linter#Define()*
Given a |String| for a filetype and a |Dictionary| Describing a linter Given a |String| for a filetype and a |Dictionary| Describing a linter
@ -985,13 +1068,13 @@ ALELint *ALELint-autocmd*
< <
=============================================================================== ===============================================================================
7. Special Thanks *ale-special-thanks* 8. Special Thanks *ale-special-thanks*
Special thanks to Mark Grealish (https://www.bhalash.com/) for providing ALE's Special thanks to Mark Grealish (https://www.bhalash.com/) for providing ALE's
snazzy looking ale glass logo. Cheers, Mark! snazzy looking ale glass logo. Cheers, Mark!
=============================================================================== ===============================================================================
8. Contact *ale-contact* 9. Contact *ale-contact*
If you like this plugin, and wish to get in touch, check out the GitHub If you like this plugin, and wish to get in touch, check out the GitHub
page for issues and more at https://github.com/w0rp/ale page for issues and more at https://github.com/w0rp/ale

View File

@ -60,6 +60,9 @@ let g:ale_filetype_blacklist = ['nerdtree', 'unite', 'tags']
" This Dictionary configures which linters are enabled for which languages. " This Dictionary configures which linters are enabled for which languages.
let g:ale_linters = get(g:, 'ale_linters', {}) let g:ale_linters = get(g:, 'ale_linters', {})
" This Dictionary configures which functions will be used for fixing problems.
let g:ale_fixers = get(g:, 'ale_fixers', {})
" This Dictionary allows users to set up filetype aliases for new filetypes. " This Dictionary allows users to set up filetype aliases for new filetypes.
let g:ale_linter_aliases = get(g:, 'ale_linter_aliases', {}) let g:ale_linter_aliases = get(g:, 'ale_linter_aliases', {})
@ -276,6 +279,11 @@ command! -bar ALEInfo :call ale#debugging#Info()
" The same, but copy output to your clipboard. " The same, but copy output to your clipboard.
command! -bar ALEInfoToClipboard :call ale#debugging#InfoToClipboard() command! -bar ALEInfoToClipboard :call ale#debugging#InfoToClipboard()
" Fix problems in files.
command! -bar ALEFix :call ale#fix#Fix()
" Suggest registered functions to use for fixing problems.
command! -bar ALEFixSuggest :call ale#fix#registry#Suggest(&filetype)
" <Plug> mappings for commands " <Plug> mappings for commands
nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return> nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return>
nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return> nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return>
@ -284,6 +292,7 @@ nnoremap <silent> <Plug>(ale_next_wrap) :ALENextWrap<Return>
nnoremap <silent> <Plug>(ale_toggle) :ALEToggle<Return> nnoremap <silent> <Plug>(ale_toggle) :ALEToggle<Return>
nnoremap <silent> <Plug>(ale_lint) :ALELint<Return> nnoremap <silent> <Plug>(ale_lint) :ALELint<Return>
nnoremap <silent> <Plug>(ale_detail) :ALEDetail<Return> nnoremap <silent> <Plug>(ale_detail) :ALEDetail<Return>
nnoremap <silent> <Plug>(ale_fix) :ALEFix<Return>
" Housekeeping " Housekeeping

187
test/test_ale_fix.vader Normal file
View File

@ -0,0 +1,187 @@
Before:
Save g:ale_fixers, &shell, g:ale_enabled
let g:ale_enabled = 0
let g:ale_run_synchronously = 1
let g:ale_fixers = {
\ 'testft': [],
\}
let &shell = '/bin/bash'
function AddCarets(buffer, lines) abort
" map() is applied to the original lines here.
" This way, we can ensure that defensive copies are made.
return map(a:lines, '''^'' . v:val')
endfunction
function AddDollars(buffer, lines) abort
return map(a:lines, '''$'' . v:val')
endfunction
function DoNothing(buffer, lines) abort
return 0
endfunction
function CatLine(buffer, lines) abort
return {'command': 'cat - <(echo d)'}
endfunction
function ReplaceWithTempFile(buffer, lines) abort
return {'command': 'echo x > %t', 'read_temporary_file': 1}
endfunction
function RemoveLastLine(buffer, lines) abort
return ['a', 'b']
endfunction
After:
Restore
unlet! g:ale_run_synchronously
unlet! g:ale_emulate_job_failure
unlet! b:ale_fixers
delfunction AddCarets
delfunction AddDollars
delfunction DoNothing
delfunction CatLine
delfunction ReplaceWithTempFile
delfunction RemoveLastLine
call ale#fix#registry#ResetToDefaults()
Given testft (A file with three lines):
a
b
c
Execute(ALEFix should complain when there are no functions to call):
AssertThrows ALEFix
AssertEqual 'Vim(echoerr):No fixers have been defined. Try :ALEFixSuggest', g:vader_exception
Execute(ALEFix should apply simple functions):
let g:ale_fixers.testft = ['AddCarets']
ALEFix
Expect(The first function should be used):
^a
^b
^c
Execute(ALEFix should apply simple functions in a chain):
let g:ale_fixers.testft = ['AddCarets', 'AddDollars']
ALEFix
Expect(Both functions should be used):
$^a
$^b
$^c
Execute(ALEFix should allow 0 to be returned to skip functions):
let g:ale_fixers.testft = ['DoNothing', 'AddDollars']
ALEFix
Expect(Only the second function should be applied):
$a
$b
$c
Execute(ALEFix should allow commands to be run):
let g:ale_fixers.testft = ['CatLine']
ALEFix
Expect(An extra line should be added):
a
b
c
d
Execute(ALEFix should allow temporary files to be read):
let g:ale_fixers.testft = ['ReplaceWithTempFile']
ALEFix
Expect(The line we wrote to the temporary file should be used here):
x
Execute(ALEFix should allow jobs and simple functions to be combined):
let g:ale_fixers.testft = ['ReplaceWithTempFile', 'AddDollars']
ALEFix
Expect(The lines from the temporary file should be modified):
$x
Execute(ALEFix should send lines modified by functions to jobs):
let g:ale_fixers.testft = ['AddDollars', 'CatLine']
ALEFix
Expect(The lines should first be modified by the function, then the job):
$a
$b
$c
d
Execute(ALEFix should skip commands when jobs fail to run):
let g:ale_emulate_job_failure = 1
let g:ale_fixers.testft = ['CatLine', 'AddDollars']
ALEFix
Expect(Only the second function should be applied):
$a
$b
$c
Execute(ALEFix should handle strings for selecting a single function):
let g:ale_fixers.testft = 'AddCarets'
ALEFix
Expect(The first function should be used):
^a
^b
^c
Execute(ALEFix should use functions from the registry):
call ale#fix#registry#Add('add_carets', 'AddCarets', [], 'Add some carets')
let g:ale_fixers.testft = ['add_carets']
ALEFix
Expect(The registry function should be used):
^a
^b
^c
Execute(ALEFix should be able to remove the last line for files):
let g:ale_fixers.testft = ['RemoveLastLine']
ALEFix
Expect(There should be only two lines):
a
b
Execute(ALEFix should accept funcrefs):
let g:ale_fixers.testft = [function('RemoveLastLine')]
ALEFix
Expect(There should be only two lines):
a
b
Execute(ALEFix should accept lambdas):
if has('nvim')
" NeoVim 0.1.7 can't interpret lambdas correctly, so just set the lines
" to make the test pass.
call setline(1, ['a', 'b', 'c', 'd'])
else
let g:ale_fixers.testft = [{buffer, lines -> lines + ['d']}]
ALEFix
endif
Expect(There should be an extra line):
a
b
c
d
Execute(ALEFix should user buffer-local fixer settings):
let g:ale_fixers.testft = ['AddCarets', 'AddDollars']
let b:ale_fixers = {'testft': ['RemoveLastLine']}
ALEFix
Expect(There should be only two lines):
a
b

View File

@ -0,0 +1,75 @@
Before:
call ale#fix#registry#Clear()
function GetSuggestions()
redir => l:output
silent ALEFixSuggest
redir END
return split(l:output, "\n")
endfunction
After:
call ale#fix#registry#ResetToDefaults()
delfunction GetSuggestions
Execute(ALEFixSuggest should return something sensible with no suggestions):
AssertEqual
\ [
\ 'There is nothing in the registry to suggest.',
\ ],
\ GetSuggestions()
Execute(ALEFixSuggest output should be correct for only generic handlers):
call ale#fix#registry#Add('zed', 'XYZ', [], 'Zedify things.')
call ale#fix#registry#Add('alpha', 'XYZ', [], 'Alpha things.')
AssertEqual
\ [
\ 'Try the following generic fixers:',
\ '',
\ '''alpha'' - Alpha things.',
\ '''zed'' - Zedify things.',
\ '',
\ 'See :help ale-fix-configuration',
\ ],
\ GetSuggestions()
Execute(ALEFixSuggest output should be correct for only filetype handlers):
let &filetype = 'testft2.testft'
call ale#fix#registry#Add('zed', 'XYZ', ['testft2'], 'Zedify things.')
call ale#fix#registry#Add('alpha', 'XYZ', ['testft'], 'Alpha things.')
AssertEqual
\ [
\ 'Try the following fixers appropriate for the filetype:',
\ '',
\ '''alpha'' - Alpha things.',
\ '''zed'' - Zedify things.',
\ '',
\ 'See :help ale-fix-configuration',
\ ],
\ GetSuggestions()
Execute(ALEFixSuggest should suggest filetype and generic handlers):
let &filetype = 'testft2.testft'
call ale#fix#registry#Add('zed', 'XYZ', ['testft2'], 'Zedify things.')
call ale#fix#registry#Add('alpha', 'XYZ', ['testft'], 'Alpha things.')
call ale#fix#registry#Add('generic', 'XYZ', [], 'Generic things.')
AssertEqual
\ [
\ 'Try the following fixers appropriate for the filetype:',
\ '',
\ '''alpha'' - Alpha things.',
\ '''zed'' - Zedify things.',
\ '',
\ 'Try the following generic fixers:',
\ '',
\ '''generic'' - Generic things.',
\ '',
\ 'See :help ale-fix-configuration',
\ ],
\ GetSuggestions()

View File

@ -11,6 +11,7 @@ Before:
\ 'valid': 1, \ 'valid': 1,
\}] \}]
let g:expected_groups = [ let g:expected_groups = [
\ 'ALEBufferFixGroup',
\ 'ALECleanupGroup', \ 'ALECleanupGroup',
\ 'ALECursorGroup', \ 'ALECursorGroup',
\ 'ALEHighlightBufferGroup', \ 'ALEHighlightBufferGroup',
@ -101,7 +102,7 @@ Execute(ALEToggle should reset everything and then run again):
AssertEqual [], getloclist(0) AssertEqual [], getloclist(0)
AssertEqual [], ale#sign#FindCurrentSigns(bufnr('%')) AssertEqual [], ale#sign#FindCurrentSigns(bufnr('%'))
AssertEqual [], getmatches() AssertEqual [], getmatches()
AssertEqual ['ALECleanupGroup', 'ALEHighlightBufferGroup'], ParseAuGroups() AssertEqual ['ALEBufferFixGroup', 'ALECleanupGroup', 'ALEHighlightBufferGroup'], ParseAuGroups()
" Toggle ALE on, everything should be set up and run again. " Toggle ALE on, everything should be set up and run again.
ALEToggle ALEToggle

View File

@ -20,7 +20,7 @@ Execute(create-react-app directories should be detected correctly):
AssertEqual AssertEqual
\ g:dir . '/eslint-test-files/react-app/node_modules/eslint/bin/eslint.js', \ g:dir . '/eslint-test-files/react-app/node_modules/eslint/bin/eslint.js',
\ ale_linters#javascript#eslint#GetExecutable(bufnr('')) \ ale#handlers#eslint#GetExecutable(bufnr(''))
:q :q
@ -31,7 +31,7 @@ Execute(use-global should override create-react-app detection):
AssertEqual AssertEqual
\ 'eslint_d', \ 'eslint_d',
\ ale_linters#javascript#eslint#GetExecutable(bufnr('')) \ ale#handlers#eslint#GetExecutable(bufnr(''))
:q :q
@ -40,7 +40,7 @@ Execute(other app directories should be detected correctly):
AssertEqual AssertEqual
\ g:dir . '/eslint-test-files/node_modules/.bin/eslint', \ g:dir . '/eslint-test-files/node_modules/.bin/eslint',
\ ale_linters#javascript#eslint#GetExecutable(bufnr('')) \ ale#handlers#eslint#GetExecutable(bufnr(''))
:q :q
@ -51,6 +51,6 @@ Execute(use-global should override other app directories):
AssertEqual AssertEqual
\ 'eslint_d', \ 'eslint_d',
\ ale_linters#javascript#eslint#GetExecutable(bufnr('')) \ ale#handlers#eslint#GetExecutable(bufnr(''))
:q :q