diff --git a/autoload/ale/events.vim b/autoload/ale/events.vim new file mode 100644 index 0000000..f8020a1 --- /dev/null +++ b/autoload/ale/events.vim @@ -0,0 +1,14 @@ +" Author: w0rp + +function! ale#events#SaveEvent() abort + let l:should_lint = g:ale_enabled && g:ale_lint_on_save + + if g:ale_fix_on_save + let l:will_fix = ale#fix#Fix('save_file') + let l:should_lint = l:should_lint && !l:will_fix + endif + + if l:should_lint + call ale#Queue(0, 'lint_file') + endif +endfunction diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index ac401f9..33f97ce 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -26,21 +26,34 @@ function! ale#fix#ApplyQueuedFixes() abort endif call remove(g:ale_fix_buffer_data, l:buffer) - call setline(1, l:data.output) - let l:start_line = len(l:data.output) + 1 - let l:end_line = len(l:data.lines_before) + if l:data.changes_made + call setline(1, l:data.output) - if l:end_line >= l:start_line - let l:save = winsaveview() - silent execute l:start_line . ',' . l:end_line . 'd' - call winrestview(l:save) + let l:start_line = len(l:data.output) + 1 + let l:end_line = len(l:data.lines_before) + + 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 l:data.should_save + set nomodified + endif + endif + + if l:data.should_save + let l:should_lint = g:ale_fix_on_save + else + let l:should_lint = l:data.changes_made 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) + if g:ale_enabled && l:should_lint + call ale#Queue(0, l:data.should_save ? 'lint_file' : '') endif endfunction @@ -49,14 +62,9 @@ function! ale#fix#ApplyFixes(buffer, output) abort let l:data = g:ale_fix_buffer_data[a:buffer] let l:data.output = a:output + let l:data.changes_made = l:data.lines_before != l:data.output - if l:data.lines_before == l:data.output - " Don't modify the buffer if nothing has changed. - call remove(g:ale_fix_buffer_data, a:buffer) - return - endif - - if bufexists(a:buffer) + if l:data.changes_made && bufexists(a:buffer) let l:lines = getbufline(a:buffer, 1, '$') if l:data.lines_before != l:lines @@ -66,7 +74,13 @@ function! ale#fix#ApplyFixes(buffer, output) abort endif let l:data.done = 1 - else + endif + + if l:data.changes_made && l:data.should_save + call writefile(a:output, l:data.filename) + endif + + if !bufexists(a:buffer) " Remove the buffer data when it doesn't exist. call remove(g:ale_fix_buffer_data, a:buffer) endif @@ -265,7 +279,6 @@ function! s:GetCallbacks() abort endfor if empty(l:callback_list) - echoerr 'No fixers have been defined. Try :ALEFixSuggest' return [] endif @@ -288,33 +301,56 @@ function! s:GetCallbacks() abort return l:corrected_list endfunction -function! ale#fix#Fix() abort +function! ale#fix#InitBufferData(buffer, fixing_flag) abort + " The 'done' flag tells the function for applying changes when fixing + " is complete. + let g:ale_fix_buffer_data[a:buffer] = { + \ 'lines_before': getbufline(a:buffer, 1, '$'), + \ 'filename': expand('#' . a:buffer . ':p'), + \ 'done': 0, + \ 'should_save': a:fixing_flag ==# 'save_file', + \ 'temporary_directory_list': [], + \} +endfunction + +" Accepts an optional argument for what to do when fixing. +" +" Returns 0 if no fixes can be applied, and 1 if fixing can be done. +function! ale#fix#Fix(...) abort + if len(a:0) > 1 + throw 'too many arguments!' + endif + + let l:fixing_flag = get(a:000, 0, '') + + if l:fixing_flag !=# '' && l:fixing_flag !=# 'save_file' + throw "fixing_flag must be either '' or 'save_file'" + endif + let l:callback_list = s:GetCallbacks() if empty(l:callback_list) - return + if l:fixing_flag ==# '' + echoerr 'No fixers have been defined. Try :ALEFixSuggest' + endif + + return 0 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 g:ale_fix_buffer_data[l:buffer] = { - \ 'lines_before': l:input, - \ 'done': 0, - \ 'temporary_directory_list': [], - \} + call ale#fix#InitBufferData(l:buffer, l:fixing_flag) call s:RunFixer({ \ 'buffer': l:buffer, - \ 'input': l:input, + \ 'input': g:ale_fix_buffer_data[l:buffer].lines_before, \ 'callback_index': 0, \ 'callback_list': l:callback_list, \}) + + return 1 endfunction " Set up an autocmd command to try and apply buffer fixes when available. diff --git a/doc/ale.txt b/doc/ale.txt index 8fb048e..1e3ac0f 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -311,6 +311,18 @@ g:ale_fixers *g:ale_fixers* This variable can be overriden with variables in each buffer. +g:ale_fix_on_save *g:ale_fix_on_save* + + Type: |Number| + Default: `0` + + When set to 1, ALE will fix files when they are saved. + + If |g:ale_lint_on_save| is set to 1, files will be checked with linters + after files are fixed, only when the buffer is open, or re-opened. Changes + to the file will saved to the file on disk. + + g:ale_history_enabled *g:ale_history_enabled* Type: |Number| @@ -770,6 +782,11 @@ 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. +Files can be fixed automatically with the following options, which are all off +by default. + +|g:ale_fix_on_save| - Fix files when they are saved. + =============================================================================== 5. Integration Documentation *ale-integrations* diff --git a/plugin/ale.vim b/plugin/ale.vim index b599154..1f9df89 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -89,6 +89,8 @@ let g:ale_lint_on_save = get(g:, 'ale_lint_on_save', 1) " This flag can be set to 1 to enable linting when the filetype is changed. let g:ale_lint_on_filetype_changed = get(g:, 'ale_lint_on_filetype_changed', 1) +call ale#Set('fix_on_save', 0) + " This flag may be set to 0 to disable ale. After ale is loaded, :ALEToggle " should be used instead. let g:ale_enabled = get(g:, 'ale_enabled', 1) @@ -218,8 +220,8 @@ function! ALEInitAuGroups() abort augroup ALERunOnSaveGroup autocmd! - if g:ale_enabled && g:ale_lint_on_save - autocmd BufWrite * call ale#Queue(0, 'lint_file') + if (g:ale_enabled && g:ale_lint_on_save) || g:ale_fix_on_save + autocmd BufWrite * call ale#events#SaveEvent() endif augroup END @@ -242,10 +244,13 @@ function! ALEInitAuGroups() abort augroup END if !g:ale_enabled + if !g:ale_fix_on_save + augroup! ALERunOnSaveGroup + endif + augroup! ALEPatternOptionsGroup augroup! ALERunOnTextChangedGroup augroup! ALERunOnEnterGroup - augroup! ALERunOnSaveGroup augroup! ALERunOnInsertLeave augroup! ALECursorGroup endif diff --git a/test/test_ale_fix.vader b/test/test_ale_fix.vader index dfe7944..b4ffc06 100644 --- a/test/test_ale_fix.vader +++ b/test/test_ale_fix.vader @@ -1,6 +1,15 @@ Before: - Save g:ale_fixers, &shell, g:ale_enabled + Save g:ale_fixers + Save &shell + Save g:ale_enabled + Save g:ale_fix_on_save + Save g:ale_lint_on_save + Save g:ale_echo_cursor + + silent! cd /testplugin/test + let g:ale_enabled = 0 + let g:ale_echo_cursor = 0 let g:ale_run_synchronously = 1 let g:ale_fixers = { \ 'testft': [], @@ -33,6 +42,19 @@ Before: return ['a', 'b'] endfunction + function! TestCallback(buffer, output) + return [{'lnum': 1, 'col': 1, 'text': 'xxx'}] + endfunction + + function! SetUpLinters() + call ale#linter#Define('testft', { + \ 'name': 'testlinter', + \ 'callback': 'TestCallback', + \ 'executable': 'true', + \ 'command': 'true', + \}) + endfunction + After: Restore unlet! g:ale_run_synchronously @@ -44,7 +66,14 @@ After: delfunction CatLine delfunction ReplaceWithTempFile delfunction RemoveLastLine + delfunction TestCallback + delfunction SetUpLinters call ale#fix#registry#ResetToDefaults() + call ale#linter#Reset() + + if filereadable('fix_test_file') + call delete('fix_test_file') + endif Given testft (A file with three lines): a @@ -185,3 +214,80 @@ Execute(ALEFix should user buffer-local fixer settings): Expect(There should be only two lines): a b + +Given testft (A file with three lines): + a + b + c + +Execute(ALEFix should save files on the save event): + let g:ale_fix_on_save = 1 + let g:ale_lint_on_save = 1 + let g:ale_enabled = 1 + + noautocmd silent file fix_test_file + + let g:ale_fixers.testft = ['AddDollars'] + + call SetUpLinters() + call ale#events#SaveEvent() + + " We should save the file. + Assert filereadable('fix_test_file'), 'The file cannot be read' + AssertEqual ['$a', '$b', '$c'], readfile('fix_test_file') + Assert !&modified, 'The was marked as ''modified''' + + " We have run the linter. + AssertEqual [{ + \ 'bufnr': bufnr('%'), + \ 'lnum': 1, + \ 'vcol': 0, + \ 'col': 1, + \ 'text': 'xxx', + \ 'type': 'E', + \ 'nr': -1, + \ 'pattern': '', + \ 'valid': 1, + \}], getloclist(0) + +Expect(The buffer should be modified): + $a + $b + $c + +Given testft (A file with three lines): + a + b + c + +Execute(ALEFix should still lint with no linters to be applied): + let g:ale_fix_on_save = 1 + let g:ale_lint_on_save = 1 + let g:ale_enabled = 1 + + noautocmd silent file fix_test_file + + let g:ale_fixers.testft = [] + + call SetUpLinters() + call ale#events#SaveEvent() + + Assert !filereadable('fix_test_file'), 'The file should not have been saved' + + " We have run the linter. + AssertEqual [{ + \ 'bufnr': bufnr('%'), + \ 'lnum': 1, + \ 'vcol': 0, + \ 'col': 1, + \ 'text': 'xxx', + \ 'type': 'E', + \ 'nr': -1, + \ 'pattern': '', + \ 'valid': 1, + \}], getloclist(0) + +Expect(The buffer should be the same): + a + b + c diff --git a/test/test_ale_init_au_groups.vader b/test/test_ale_init_au_groups.vader index 0134f76..532232b 100644 --- a/test/test_ale_init_au_groups.vader +++ b/test/test_ale_init_au_groups.vader @@ -31,6 +31,7 @@ Before: return l:matches endfunction + Save g:ale_enabled Save g:ale_lint_on_text_changed Save g:ale_lint_on_insert_leave Save g:ale_pattern_options_enabled @@ -38,6 +39,7 @@ Before: Save g:ale_lint_on_filetype_changed Save g:ale_lint_on_save Save g:ale_echo_cursor + Save g:ale_fix_on_save After: delfunction CheckAutocmd @@ -138,14 +140,33 @@ Execute (g:ale_lint_on_filetype_changed = 1 should bind FileType, and required b Execute (g:ale_lint_on_save = 0 should bind no events): let g:ale_lint_on_save = 0 + let g:ale_fix_on_save = 0 AssertEqual [], CheckAutocmd('ALERunOnSaveGroup') Execute (g:ale_lint_on_save = 1 should bind no events): let g:ale_lint_on_save = 1 + let g:ale_fix_on_save = 0 AssertEqual [ - \ 'BufWritePre * call ale#Queue(0, ''lint_file'')', + \ 'BufWritePre * call ale#events#SaveEvent()', + \], CheckAutocmd('ALERunOnSaveGroup') + +Execute (g:ale_lint_on_save = 0 and g:ale_fix_on_save = 1 should bind events): + let g:ale_lint_on_save = 0 + let g:ale_fix_on_save = 1 + + AssertEqual [ + \ 'BufWritePre * call ale#events#SaveEvent()', + \], CheckAutocmd('ALERunOnSaveGroup') + +Execute (g:ale_fix_on_save = 1 should bind events even when ALE is disabled): + let g:ale_enabled = 0 + let g:ale_lint_on_save = 0 + let g:ale_fix_on_save = 1 + + AssertEqual [ + \ 'BufWritePre * call ale#events#SaveEvent()', \], CheckAutocmd('ALERunOnSaveGroup') Execute (g:ale_echo_cursor = 0 should bind no events):