From 7d8390d43e83f3e097469fd3e4f65f07a3035903 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 18 May 2017 01:58:27 +0100 Subject: [PATCH 01/16] Add experimental code for fixing errors --- ale_linters/javascript/eslint.vim | 30 +--- autoload/ale/fix.vim | 227 ++++++++++++++++++++++++++++++ autoload/ale/handlers/eslint.vim | 43 ++++++ 3 files changed, 272 insertions(+), 28 deletions(-) create mode 100644 autoload/ale/fix.vim create mode 100644 autoload/ale/handlers/eslint.vim diff --git a/ale_linters/javascript/eslint.vim b/ale_linters/javascript/eslint.vim index f1c3bb0..9fd2007 100644 --- a/ale_linters/javascript/eslint.vim +++ b/ale_linters/javascript/eslint.vim @@ -1,40 +1,14 @@ " Author: w0rp " Description: eslint for JavaScript files -let g:ale_javascript_eslint_executable = -\ get(g:, 'ale_javascript_eslint_executable', 'eslint') - let g:ale_javascript_eslint_options = \ get(g:, 'ale_javascript_eslint_options', '') let g:ale_javascript_eslint_use_global = \ 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 - return ale#Escape(ale_linters#javascript#eslint#GetExecutable(a:buffer)) + return ale#handlers#eslint#GetExecutable(a:buffer) \ . ' ' . ale#Var(a:buffer, 'javascript_eslint_options') \ . ' -f unix --stdin --stdin-filename %s' endfunction @@ -103,7 +77,7 @@ endfunction call ale#linter#Define('javascript', { \ 'name': 'eslint', -\ 'executable_callback': 'ale_linters#javascript#eslint#GetExecutable', +\ 'executable_callback': 'ale#handlers#eslint#GetExecutable', \ 'command_callback': 'ale_linters#javascript#eslint#GetCommand', \ 'callback': 'ale_linters#javascript#eslint#Handle', \}) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim new file mode 100644 index 0000000..50a426b --- /dev/null +++ b/autoload/ale/fix.vim @@ -0,0 +1,227 @@ +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 + + echom l:data.output[0] + + 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 +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) 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(getbufline(a:buffer, 1, '$'), 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: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) + + 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 + + let l:job_id = ale#job#Start(l:command, l:job_options) + + " TODO: Check that the job runs, and skip to the next item if it does not. + + let s:job_info_map[l:job_id] = l:job_info +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 = function(a:options.callback_list[l:index])(l:buffer, 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 + " TODO: Check the return value here, and skip an index if + " the job fails. + call s:RunJob({ + \ 'buffer': l:buffer, + \ 'command': l:result.command, + \ '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, + \}) + + " Stop here, we will handle exit later on. + return + endif + endwhile + + call s:ApplyFixes(l:buffer, l:input) +endfunction + +function! ale#fix#Fix() abort + let l:callback_list = [] + + for l:sub_type in split(&filetype, '\.') + call extend(l:callback_list, get(g:ale_fixers, l:sub_type, [])) + endfor + + if empty(l:callback_list) + echoerr 'No fixers have been defined for filetype: ' . &filetype + 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/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim new file mode 100644 index 0000000..a7e8ef4 --- /dev/null +++ b/autoload/ale/handlers/eslint.vim @@ -0,0 +1,43 @@ +" Author: w0rp +" 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 From 8ebd15a54dba474ee634e0087bb460ca6e7d8428 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 18 May 2017 13:21:14 +0100 Subject: [PATCH 02/16] Add commands to run ALEFix, and some tests to cover functionality so far. Add a simple autopep8 function. --- autoload/ale/engine.vim | 3 +- autoload/ale/fix.vim | 55 ++++++++-- autoload/ale/handlers/python.vim | 6 ++ plugin/ale.vim | 7 ++ test/test_ale_fix.vader | 109 ++++++++++++++++++++ test/test_ale_toggle.vader | 3 +- test/test_eslint_executable_detection.vader | 8 +- 7 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 test/test_ale_fix.vader diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index af074c0..e13562a 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 index 50a426b..6ed750c 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -1,3 +1,6 @@ +" 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 = {} @@ -23,8 +26,6 @@ function! ale#fix#ApplyQueuedFixes() abort return endif - echom l:data.output[0] - call setline(1, l:data.output) let l:start_line = len(l:data.output) + 1 @@ -145,11 +146,42 @@ function! s:RunJob(options) abort let l:job_options.out_cb = function('s:GatherOutput') endif - let l:job_id = ale#job#Start(l:command, l:job_options) + 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 - " TODO: Check that the job runs, and skip to the next item if it does not. + 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 @@ -158,7 +190,7 @@ function! s:RunFixer(options) abort let l:index = a:options.callback_index while len(a:options.callback_list) > l:index - let l:result = function(a:options.callback_list[l:index])(l:buffer, l:input) + let l:result = function(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. @@ -167,9 +199,7 @@ function! s:RunFixer(options) abort let l:input = l:result let l:index += 1 else - " TODO: Check the return value here, and skip an index if - " the job fails. - call s:RunJob({ + let l:job_ran = s:RunJob({ \ 'buffer': l:buffer, \ 'command': l:result.command, \ 'output_stream': get(l:result, 'output_stream', 'stdout'), @@ -178,8 +208,13 @@ function! s:RunFixer(options) abort \ 'callback_index': l:index, \}) - " Stop here, we will handle exit later on. - return + 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 diff --git a/autoload/ale/handlers/python.vim b/autoload/ale/handlers/python.vim index 85e2f20..33ee3c9 100644 --- a/autoload/ale/handlers/python.vim +++ b/autoload/ale/handlers/python.vim @@ -35,3 +35,9 @@ function! ale#handlers#python#HandlePEP8Format(buffer, lines) abort return l:output endfunction + +function! ale#handlers#python#AutoPEP8(buffer, lines) abort + return { + \ 'command': 'autopep8 -' + \} +endfunction diff --git a/plugin/ale.vim b/plugin/ale.vim index 0e8c369..28b8beb 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -60,6 +60,9 @@ let g:ale_filetype_blacklist = ['nerdtree', 'unite', 'tags'] " This Dictionary configures which linters are enabled for which languages. 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. let g:ale_linter_aliases = get(g:, 'ale_linter_aliases', {}) @@ -276,6 +279,9 @@ command! -bar ALEInfo :call ale#debugging#Info() " The same, but copy output to your clipboard. command! -bar ALEInfoToClipboard :call ale#debugging#InfoToClipboard() +" Fix problems in files. +command! -bar ALEFix :call ale#fix#Fix() + " mappings for commands nnoremap (ale_previous) :ALEPrevious nnoremap (ale_previous_wrap) :ALEPreviousWrap @@ -284,6 +290,7 @@ nnoremap (ale_next_wrap) :ALENextWrap nnoremap (ale_toggle) :ALEToggle nnoremap (ale_lint) :ALELint nnoremap (ale_detail) :ALEDetail +nnoremap (ale_fix) :ALEFix " Housekeeping diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader new file mode 100644 index 0000000..50e0e06 --- /dev/null +++ b/test/test_ale_fix.vader @@ -0,0 +1,109 @@ +Before: + Save g:ale_fixers, &shell + 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 + +After: + Restore + unlet! g:ale_run_synchronously + unlet! g:ale_emulate_job_failure + delfunction AddCarets + delfunction AddDollars + delfunction DoNothing + delfunction CatLine + delfunction ReplaceWithTempFile + +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 for filetype: testft', 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 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 diff --git a/test/test_ale_toggle.vader b/test/test_ale_toggle.vader index 5d27c86..3546ad7 100644 --- a/test/test_ale_toggle.vader +++ b/test/test_ale_toggle.vader @@ -11,6 +11,7 @@ Before: \ 'valid': 1, \}] let g:expected_groups = [ + \ 'ALEBufferFixGroup', \ 'ALECleanupGroup', \ 'ALECursorGroup', \ 'ALEHighlightBufferGroup', @@ -101,7 +102,7 @@ Execute(ALEToggle should reset everything and then run again): AssertEqual [], getloclist(0) AssertEqual [], ale#sign#FindCurrentSigns(bufnr('%')) AssertEqual [], getmatches() - AssertEqual ['ALECleanupGroup', 'ALEHighlightBufferGroup'], ParseAuGroups() + AssertEqual ['ALEBufferFixGroup', 'ALECleanupGroup', 'ALEHighlightBufferGroup'], ParseAuGroups() " Toggle ALE on, everything should be set up and run again. ALEToggle diff --git a/test/test_eslint_executable_detection.vader b/test/test_eslint_executable_detection.vader index e963ae1..03bb89e 100644 --- a/test/test_eslint_executable_detection.vader +++ b/test/test_eslint_executable_detection.vader @@ -20,7 +20,7 @@ Execute(create-react-app directories should be detected correctly): AssertEqual \ 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 @@ -31,7 +31,7 @@ Execute(use-global should override create-react-app detection): AssertEqual \ 'eslint_d', - \ ale_linters#javascript#eslint#GetExecutable(bufnr('')) + \ ale#handlers#eslint#GetExecutable(bufnr('')) :q @@ -40,7 +40,7 @@ Execute(other app directories should be detected correctly): AssertEqual \ g:dir . '/eslint-test-files/node_modules/.bin/eslint', - \ ale_linters#javascript#eslint#GetExecutable(bufnr('')) + \ ale#handlers#eslint#GetExecutable(bufnr('')) :q @@ -51,6 +51,6 @@ Execute(use-global should override other app directories): AssertEqual \ 'eslint_d', - \ ale_linters#javascript#eslint#GetExecutable(bufnr('')) + \ ale#handlers#eslint#GetExecutable(bufnr('')) :q From 05bab00c3c9878229e8b3cb8df3dc66a7ad9ee7f Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 18 May 2017 17:26:17 +0100 Subject: [PATCH 03/16] Allow strings to be used for selecting a single fix function for g:ale_fixers too --- autoload/ale/fix.vim | 8 +++++++- test/test_ale_fix.vader | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 6ed750c..70a36ed 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -225,7 +225,13 @@ function! ale#fix#Fix() abort let l:callback_list = [] for l:sub_type in split(&filetype, '\.') - call extend(l:callback_list, get(g:ale_fixers, l:sub_type, [])) + let l:sub_type_callacks = get(g:ale_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) diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 50e0e06..95a37c6 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -107,3 +107,12 @@ 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 From 0b743389e526caa7c9065405917da84f83a59b17 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 18 May 2017 17:50:20 +0100 Subject: [PATCH 04/16] Send modified lines to jobs, not the file contents --- autoload/ale/fix.vim | 8 +++++--- test/test_ale_fix.vader | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 70a36ed..288919a 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -94,7 +94,7 @@ function! ale#fix#RemoveManagedFiles(buffer) abort let s:buffer_data[a:buffer].temporary_directory_list = [] endfunction -function! s:CreateTemporaryFileForJob(buffer, temporary_file) abort +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 @@ -107,7 +107,7 @@ function! s:CreateTemporaryFileForJob(buffer, temporary_file) abort " Automatically delete the directory later. call ale#fix#ManageDirectory(a:buffer, l:temporary_directory) " Write the buffer out to a file. - call writefile(getbufline(a:buffer, 1, '$'), a:temporary_file) + call writefile(a:input, a:temporary_file) return 1 endfunction @@ -115,11 +115,12 @@ 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) + call s:CreateTemporaryFileForJob(l:buffer, l:temporary_file, l:input) let l:command = ale#job#PrepareCommand(l:command) let l:job_options = { @@ -202,6 +203,7 @@ function! s:RunFixer(options) abort 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, diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 95a37c6..8ec7896 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -98,6 +98,16 @@ Execute(ALEFix should allow jobs and simple functions to be combined): 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'] From ea1627f5ce5620806644a525f5dc8523187fd69f Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 18 May 2017 17:50:39 +0100 Subject: [PATCH 05/16] Start experimenting with generic functions for fixing problems --- autoload/ale/fix/generic.vim | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 autoload/ale/fix/generic.vim diff --git a/autoload/ale/fix/generic.vim b/autoload/ale/fix/generic.vim new file mode 100644 index 0000000..5c5b200 --- /dev/null +++ b/autoload/ale/fix/generic.vim @@ -0,0 +1,12 @@ +" Author: w0rp +" 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 From 1f4d1800e0040d7d36d1c19e15c5f0e570122273 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 18 May 2017 23:50:06 +0100 Subject: [PATCH 06/16] Allow function aliases to be registered for fixing problems, and add some more argument checking for fixing problems --- autoload/ale/fix.vim | 38 ++++++++++++++++++++++-- autoload/ale/fix/registry.vim | 54 +++++++++++++++++++++++++++++++++++ test/test_ale_fix.vader | 16 +++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 autoload/ale/fix/registry.vim diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 288919a..89778a1 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -191,7 +191,7 @@ function! s:RunFixer(options) abort let l:index = a:options.callback_index while len(a:options.callback_list) > l:index - let l:result = function(a:options.callback_list[l:index])(l:buffer, copy(l:input)) + 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. @@ -223,7 +223,7 @@ function! s:RunFixer(options) abort call s:ApplyFixes(l:buffer, l:input) endfunction -function! ale#fix#Fix() abort +function! s:GetCallbacks() abort let l:callback_list = [] for l:sub_type in split(&filetype, '\.') @@ -238,6 +238,40 @@ function! ale#fix#Fix() abort if empty(l:callback_list) echoerr 'No fixers have been defined for filetype: ' . &filetype + return [] + endif + + let l:problem_list = [] + let l:corrected_list = [] + + for l:item in l:callback_list + if type(l:item) == type('') + if exists('*' . l:item) + call add(l:corrected_list, function(l:item)) + else + let l:func = ale#fix#registry#GetFunc(l:item) + + if !empty(l:func) && exists('*' . l:func) + call add(l:corrected_list, function(l:func)) + else + call add(l:problem_list, l:item) + endif + endif + endif + endfor + + if !empty(l:problem_list) + echoerr 'Invalid fixers used: ' . string(l:problem_list) + return [] + endif + + return l:corrected_list +endfunction + +function! ale#fix#Fix() abort + let l:callback_list = s:GetCallbacks() + + if empty(l:callback_list) return endif diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim new file mode 100644 index 0000000..b0f87dd --- /dev/null +++ b/autoload/ale/fix/registry.vim @@ -0,0 +1,54 @@ +" Author: w0rp +" Description: A registry of functions for fixing things. + +let s:default_registry = { +\ 'eslint': { +\ 'function': 'ale#handlers#eslint#Fix', +\ 'suggested_filetypes': ['javascript'], +\ 'description': '', +\ }, +\} + +" 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() + +" 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 diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 8ec7896..a872f38 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -37,6 +37,7 @@ After: delfunction DoNothing delfunction CatLine delfunction ReplaceWithTempFile + call ale#fix#registry#ResetToDefaults() Given testft (A file with three lines): a @@ -126,3 +127,18 @@ Expect(The first function should be used): ^a ^b ^c + +Execute(ALEFix should complain for missing functions): + let g:ale_fixers.testft = ['XXX', 'YYY'] + AssertThrows ALEFix + AssertEqual 'Vim(echoerr):Invalid fixers used: [''XXX'', ''YYY'']', g:vader_exception + +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 From 4214832ae263086d1aa1f565067d00e9ed1b820e Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 19 May 2017 09:49:00 +0100 Subject: [PATCH 07/16] Remove the code for checking if functions exist. It breaks autoload functions --- autoload/ale/fix.vim | 20 +++++--------------- test/test_ale_fix.vader | 5 ----- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 89778a1..b2ca257 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -241,29 +241,19 @@ function! s:GetCallbacks() abort return [] endif - let l:problem_list = [] let l:corrected_list = [] for l:item in l:callback_list if type(l:item) == type('') - if exists('*' . l:item) - call add(l:corrected_list, function(l:item)) - else - let l:func = ale#fix#registry#GetFunc(l:item) + let l:func = ale#fix#registry#GetFunc(l:item) - if !empty(l:func) && exists('*' . l:func) - call add(l:corrected_list, function(l:func)) - else - call add(l:problem_list, l:item) - endif + if !empty(l:func) + let l:item = l:func endif endif - endfor - if !empty(l:problem_list) - echoerr 'Invalid fixers used: ' . string(l:problem_list) - return [] - endif + call add(l:corrected_list, function(l:item)) + endfor return l:corrected_list endfunction diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index a872f38..8e61aef 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -128,11 +128,6 @@ Expect(The first function should be used): ^b ^c -Execute(ALEFix should complain for missing functions): - let g:ale_fixers.testft = ['XXX', 'YYY'] - AssertThrows ALEFix - AssertEqual 'Vim(echoerr):Invalid fixers used: [''XXX'', ''YYY'']', g:vader_exception - 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'] From e6b132c915f11e7ff4962f14bfeba1bd77cd5f9f Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 19 May 2017 09:53:28 +0100 Subject: [PATCH 08/16] Fix an off-by-one bug in ALEFix --- autoload/ale/fix.vim | 2 +- test/test_ale_fix.vader | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index b2ca257..a674e75 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -31,7 +31,7 @@ function! ale#fix#ApplyQueuedFixes() abort let l:start_line = len(l:data.output) + 1 let l:end_line = len(l:lines) - if l:end_line > l:start_line + if l:end_line >= l:start_line let l:save = winsaveview() silent execute l:start_line . ',' . l:end_line . 'd' call winrestview(l:save) diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 8e61aef..71fd84f 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -28,6 +28,9 @@ Before: 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 @@ -37,6 +40,7 @@ After: delfunction DoNothing delfunction CatLine delfunction ReplaceWithTempFile + delfunction RemoveLastLine call ale#fix#registry#ResetToDefaults() Given testft (A file with three lines): @@ -137,3 +141,11 @@ 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 From 18467a55b527358613589ed087c5a308fb37b898 Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 19 May 2017 15:23:00 +0100 Subject: [PATCH 09/16] Don't modify files when fixing doesn't change anything. --- autoload/ale/fix.vim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index a674e75..9fe9956 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -26,6 +26,11 @@ function! ale#fix#ApplyQueuedFixes() abort 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 From 74691269ce7050e6c13053bd884af9c05b630c1e Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 19 May 2017 15:24:21 +0100 Subject: [PATCH 10/16] Run a lint cycle after fixing problems --- autoload/ale/fix.vim | 6 ++++++ test/test_ale_fix.vader | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 9fe9956..abb6afe 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -41,6 +41,12 @@ function! ale#fix#ApplyQueuedFixes() abort 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 diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 71fd84f..04657e9 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -1,5 +1,6 @@ Before: - Save g:ale_fixers, &shell + Save g:ale_fixers, &shell, g:ale_enabled + let g:ale_enabled = 0 let g:ale_run_synchronously = 1 let g:ale_fixers = { \ 'testft': [], @@ -31,6 +32,7 @@ Before: function RemoveLastLine(buffer, lines) abort return ['a', 'b'] endfunction + After: Restore unlet! g:ale_run_synchronously From e80389f8d453c610e9d6f7c1acf7085ad77abc19 Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 19 May 2017 15:24:41 +0100 Subject: [PATCH 11/16] Add some more tools for fixing problems with Python files --- autoload/ale/fix/registry.vim | 22 +++++++++++++++++++++- autoload/ale/handlers/python.vim | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index b0f87dd..e524e13 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -2,10 +2,30 @@ " 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': '', +\ '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.', \ }, \} diff --git a/autoload/ale/handlers/python.vim b/autoload/ale/handlers/python.vim index 33ee3c9..5e9ddec 100644 --- a/autoload/ale/handlers/python.vim +++ b/autoload/ale/handlers/python.vim @@ -41,3 +41,25 @@ function! ale#handlers#python#AutoPEP8(buffer, lines) abort \ '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 From ed097cfcbd5c52835c27632f1e3ac52d2fe0b11a Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 19 May 2017 15:44:52 +0100 Subject: [PATCH 12/16] Allow funcref values and lambdas for ALEFix --- autoload/ale/fix.vim | 14 ++++++++------ test/test_ale_fix.vader | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index abb6afe..9d0145c 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -254,16 +254,18 @@ function! s:GetCallbacks() abort let l:corrected_list = [] - for l:item in l:callback_list - if type(l:item) == type('') - let l:func = ale#fix#registry#GetFunc(l:item) + " 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 + if !empty(l:Func) + let l:Item = l:Func endif endif - call add(l:corrected_list, function(l:item)) + call add(l:corrected_list, function(l:Item)) endfor return l:corrected_list diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 04657e9..49d0d2d 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -151,3 +151,27 @@ Execute(ALEFix should be able to remove the last line for files): 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 From ad52b9630d95a9cf684872ec3614d650b75ed935 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 20 May 2017 15:52:42 +0100 Subject: [PATCH 13/16] Fix Funcref fixers for NeoVim --- autoload/ale/fix.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 9d0145c..53c3fd2 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -265,7 +265,7 @@ function! s:GetCallbacks() abort endif endif - call add(l:corrected_list, function(l:Item)) + call add(l:corrected_list, ale#util#GetFunction(l:Item)) endfor return l:corrected_list From 59d9f5d458036bb6a2fbb0d3c3e301f4717eb916 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 20 May 2017 16:00:05 +0100 Subject: [PATCH 14/16] Allow b:ale_fixers to be used --- autoload/ale/fix.vim | 3 ++- test/test_ale_fix.vader | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 53c3fd2..4ff977b 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -235,10 +235,11 @@ function! s:RunFixer(options) abort 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(g:ale_fixers, l:sub_type, []) + 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) diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 49d0d2d..23c61f9 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -37,6 +37,7 @@ After: Restore unlet! g:ale_run_synchronously unlet! g:ale_emulate_job_failure + unlet! b:ale_fixers delfunction AddCarets delfunction AddDollars delfunction DoNothing @@ -175,3 +176,12 @@ Expect(There should be an extra line): 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 From 3530180a73ec53c6c029926173c34e0d78a8ac70 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 20 May 2017 18:56:44 +0100 Subject: [PATCH 15/16] Suggest functions for fixing issues for ALEFix --- autoload/ale/fix.vim | 2 +- autoload/ale/fix/registry.vim | 60 ++++++++++++++++++++++++++ plugin/ale.vim | 2 + test/test_ale_fix.vader | 2 +- test/test_ale_fix_suggest.vader | 75 +++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 test/test_ale_fix_suggest.vader diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 4ff977b..e329693 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -249,7 +249,7 @@ function! s:GetCallbacks() abort endfor if empty(l:callback_list) - echoerr 'No fixers have been defined for filetype: ' . &filetype + echoerr 'No fixers have been defined. Try :ALEFixSuggest' return [] endif diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index e524e13..b85c5d7 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -37,6 +37,11 @@ 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('') @@ -72,3 +77,58 @@ endfunction 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/plugin/ale.vim b/plugin/ale.vim index 28b8beb..a1a8666 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -281,6 +281,8 @@ 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) " mappings for commands nnoremap (ale_previous) :ALEPrevious diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index 23c61f9..dfe7944 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -53,7 +53,7 @@ Given testft (A file with three lines): Execute(ALEFix should complain when there are no functions to call): AssertThrows ALEFix - AssertEqual 'Vim(echoerr):No fixers have been defined for filetype: testft', g:vader_exception + 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'] diff --git a/test/test_ale_fix_suggest.vader b/test/test_ale_fix_suggest.vader new file mode 100644 index 0000000..9a7aecb --- /dev/null +++ b/test/test_ale_fix_suggest.vader @@ -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() From 74d879952cfa3a27b21869bdbfef909c793178bb Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 20 May 2017 19:01:12 +0100 Subject: [PATCH 16/16] Document ALEFix --- README.md | 6 +++ doc/ale.txt | 105 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cc9671b..06b3cdd 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ back to a filesystem. 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 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 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`. + ## 3. Installation diff --git a/doc/ale.txt b/doc/ale.txt index 74368c9..f88fbbc 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -9,7 +9,8 @@ CONTENTS *ale-contents* 1. Introduction.........................|ale-introduction| 2. Supported Languages & Tools..........|ale-support| 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| gcc.................................|ale-asm-gcc| c.....................................|ale-c-options| @@ -93,10 +94,10 @@ CONTENTS *ale-contents* xmllint.............................|ale-xml-xmllint| yaml..................................|ale-yaml-options| yamllint............................|ale-yaml-yamllint| - 5. Commands/Keybinds....................|ale-commands| - 6. API..................................|ale-api| - 7. Special Thanks.......................|ale-special-thanks| - 8. Contact..............................|ale-contact| + 6. Commands/Keybinds....................|ale-commands| + 7. API..................................|ale-api| + 8. Special Thanks.......................|ale-special-thanks| + 9. Contact..............................|ale-contact| =============================================================================== 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 as a minimum. -ALE supports the following key features: +ALE supports the following key features for linting: 1. Running linters when text is changed. 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. 5. Setting |signs| with warnings and errors for error markers. 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* @@ -266,6 +271,18 @@ g:ale_enabled *g:ale_enabled* 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* 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 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* @@ -676,7 +748,7 @@ ALEDetail *ALEDetail* A plug mapping `(ale_detail)` is defined for this command. =============================================================================== -6. API *ale-api* +7. API *ale-api* ale#Queue(delay, [linting_flag]) *ale#Queue()* @@ -745,6 +817,17 @@ ale#engine#ManageDirectory(buffer, directory) *ale#engine#ManageDirectory()* 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()* 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 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 page for issues and more at https://github.com/w0rp/ale