diff --git a/autoload/ale.vim b/autoload/ale.vim index ce2589b..830a281 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -3,7 +3,7 @@ " Manages execution of linters when requested by autocommands let s:lint_timer = -1 -let s:should_lint_file = 0 +let s:should_lint_file_for_buffer = {} " A function for checking various conditions whereby ALE just shouldn't " attempt to do anything, say if particular buffer types are open in Vim. @@ -20,8 +20,8 @@ function! ale#Queue(delay, ...) abort throw 'too many arguments!' endif - " Default run_file_linters to 0 - let l:linting_flag = len(a:0) > 1 ? a:1 : '' + " Default linting_flag to '' + let l:linting_flag = get(a:000, 0, '') if l:linting_flag !=# '' && l:linting_flag !=# 'lint_file' throw "linting_flag must be either '' or 'lint_file'" @@ -31,8 +31,11 @@ function! ale#Queue(delay, ...) abort return endif - " Remember the event used for linting. - let s:should_lint_file = l:linting_flag ==# 'lint_file' + " Remember that we want to check files for this buffer. + " We will remember this until we finally run the linters, via any event. + if l:linting_flag ==# 'lint_file' + let s:should_lint_file_for_buffer[bufnr('%')] = 1 + endif if s:lint_timer != -1 call timer_stop(s:lint_timer) @@ -59,6 +62,13 @@ function! ale#Lint(...) abort let l:buffer = bufnr('%') let l:linters = ale#linter#Get(&filetype) + let l:should_lint_file = 0 + + " Check if we previously requested checking the file. + if has_key(s:should_lint_file_for_buffer, l:buffer) + unlet s:should_lint_file_for_buffer[l:buffer] + let l:should_lint_file = 1 + endif " Initialise the buffer information if needed. call ale#engine#InitBufferInfo(l:buffer) @@ -66,7 +76,22 @@ function! ale#Lint(...) abort " Clear the new loclist again, so we will work with all new items. let g:ale_buffer_info[l:buffer].new_loclist = [] + if l:should_lint_file + " Clear loclist items for files if we are checking files again. + let g:ale_buffer_info[l:buffer].lint_file_loclist = [] + else + " Otherwise, don't run any `lint_file` linters + " We will continue running any linters which are currently checking + " the file, and the items will be mixed together with any new items. + call filter(l:linters, '!v:val.lint_file') + endif + for l:linter in l:linters call ale#engine#Invoke(l:buffer, l:linter) endfor endfunction + +" Reset flags indicating that files should be checked for all buffers. +function! ale#ResetLintFileMarkers() abort + let s:should_lint_file_for_buffer = {} +endfunction diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 8f5a06a..3e440d1 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -226,7 +226,13 @@ function! s:HandleExit(job) abort let l:linter_loclist = ale#engine#FixLocList(l:buffer, l:linter, l:linter_loclist) " Add the loclist items from the linter. - call extend(g:ale_buffer_info[l:buffer].new_loclist, l:linter_loclist) + " loclist items for files which are checked go into a different list, + " and are kept between runs. + if l:linter.lint_file + call extend(g:ale_buffer_info[l:buffer].lint_file_loclist, l:linter_loclist) + else + call extend(g:ale_buffer_info[l:buffer].new_loclist, l:linter_loclist) + endif if !empty(g:ale_buffer_info[l:buffer].job_list) " Wait for all jobs to complete before doing anything else. @@ -237,14 +243,18 @@ function! s:HandleExit(job) abort " now that all jobs have completed. call ale#engine#RemoveManagedFiles(l:buffer) + " Combine the lint_file List and the List for everything else. + let l:combined_list = g:ale_buffer_info[l:buffer].lint_file_loclist + \ + g:ale_buffer_info[l:buffer].new_loclist + " Sort the loclist again. " We need a sorted list so we can run a binary search against it " for efficient lookup of the messages in the cursor handler. - call sort(g:ale_buffer_info[l:buffer].new_loclist, 'ale#util#LocItemCompare') + call sort(l:combined_list, 'ale#util#LocItemCompare') " Now swap the old and new loclists, after we have collected everything " and sorted the list again. - let g:ale_buffer_info[l:buffer].loclist = g:ale_buffer_info[l:buffer].new_loclist + let g:ale_buffer_info[l:buffer].loclist = l:combined_list let g:ale_buffer_info[l:buffer].new_loclist = [] call ale#engine#SetResults(l:buffer, g:ale_buffer_info[l:buffer].loclist) @@ -255,6 +265,7 @@ endfunction function! ale#engine#SetResults(buffer, loclist) abort " Set signs first. This could potentially fix some line numbers. + " The List could be sorted again here by SetSigns. if g:ale_set_signs call ale#sign#SetSigns(a:buffer, a:loclist) endif @@ -698,10 +709,14 @@ function! ale#engine#WaitForJobs(deadline) abort " for command_chain linters. let l:has_new_jobs = 0 + " Check again to see if any jobs are running. for l:info in values(g:ale_buffer_info) - if !empty(l:info.job_list) - let l:has_new_jobs = 1 - endif + for l:job in l:info.job_list + if job_status(l:job) ==# 'run' + let l:has_new_jobs = 1 + break + endif + endfor endfor if l:has_new_jobs diff --git a/doc/ale.txt b/doc/ale.txt index c9b51f0..bf700e0 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1407,6 +1407,24 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* if a command manually reads from a temporary file instead, etc. + `lint_file` A |Number| (`0` or `1`) indicating whether a command + should read the file instead of the Vim buffer. This + option can be used for linters which must check the + file on disk, and which cannot check a Vim buffer + instead. + + Linters set with this option will not be run as a + user types, per |g:ale_lint_on_text_changed|. Linters + will instead be run only when events occur against + the file on disk, including |g:ale_lint_on_enter| + and |g:ale_lint_on_save|. Linters with this option + set to `1` will also be run when linters are run + manually, per |ALELint-autocmd|. + + When this option is set to `1`, `read_buffer` will + be set automatically to `0`. The two options cannot + be used together. + Only one of `command`, `command_callback`, or `command_chain` should be specified. `command_callback` is generally recommended when a command string needs to be generated dynamically, or any global options are used. diff --git a/test/test_lint_file_linters.vader b/test/test_lint_file_linters.vader new file mode 100644 index 0000000..d80fe2c --- /dev/null +++ b/test/test_lint_file_linters.vader @@ -0,0 +1,174 @@ +Before: + let g:buffer_result = [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'text': 'buffer error', + \ 'type': 'E', + \ }, + \ { + \ 'lnum': 2, + \ 'col': 1, + \ 'text': 'buffer warning', + \ 'type': 'W', + \ }, + \] + + function! LintFileCallback(buffer, output) + return [ + \ { + \ 'lnum': 1, + \ 'col': 3, + \ 'text': 'file warning', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 2, + \ 'col': 3, + \ 'text': 'file error', + \ 'type': 'E', + \ }, + \] + endfunction + + function! BufferCallback(buffer, output) + return deepcopy(g:buffer_result) + endfunction + + function! GetSimplerLoclist() + let l:loclist = [] + + for l:item in getloclist(0) + call add(l:loclist, { + \ 'lnum': l:item.lnum, + \ 'col': l:item.col, + \ 'text': l:item.text, + \ 'type': l:item.type, + \}) + endfor + + return l:loclist + endfunction + + call ale#linter#Define('foobar', { + \ 'name': 'lint_file_linter', + \ 'callback': 'LintFileCallback', + \ 'executable': 'echo', + \ 'command': 'echo', + \ 'lint_file': 1, + \}) + + call ale#linter#Define('foobar', { + \ 'name': 'buffer_linter', + \ 'callback': 'BufferCallback', + \ 'executable': 'echo', + \ 'command': 'echo', + \ 'read_buffer': 0, + \}) + +After: + unlet g:buffer_result + let g:ale_buffer_info = {} + call ale#linter#Reset() + call setloclist(0, []) + delfunction LintFileCallback + delfunction BufferCallback + +Given foobar (Some imaginary filetype): + foo + bar + baz + +Execute(Running linters without 'lint_file' should run only buffer linters): + call ale#ResetLintFileMarkers() + let g:ale_buffer_info = {} + call ale#Queue(0) + call ale#engine#WaitForJobs(2000) + + AssertEqual [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'text': 'buffer error', + \ 'type': 'E', + \ }, + \ { + \ 'lnum': 2, + \ 'col': 1, + \ 'text': 'buffer warning', + \ 'type': 'W', + \ }, + \], GetSimplerLoclist() + +Execute(Running linters with 'lint_file' should run all linters): + call ale#ResetLintFileMarkers() + let g:ale_buffer_info = {} + call ale#Queue(0, 'lint_file') + call ale#engine#WaitForJobs(2000) + + AssertEqual [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'text': 'buffer error', + \ 'type': 'E', + \ }, + \ { + \ 'lnum': 1, + \ 'col': 3, + \ 'text': 'file warning', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 2, + \ 'col': 1, + \ 'text': 'buffer warning', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 2, + \ 'col': 3, + \ 'text': 'file error', + \ 'type': 'E', + \ }, + \], GetSimplerLoclist() + +Execute(Linter errors from files should be kept): + call ale#ResetLintFileMarkers() + let g:ale_buffer_info = {} + call ale#Queue(0, 'lint_file') + call ale#engine#WaitForJobs(2000) + + " Change the results for the buffer callback. + let g:buffer_result = [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'text': 'new buffer error', + \ 'type': 'E', + \ }, + \] + + call ale#Queue(0) + call ale#engine#WaitForJobs(2000) + + AssertEqual [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'text': 'new buffer error', + \ 'type': 'E', + \ }, + \ { + \ 'lnum': 1, + \ 'col': 3, + \ 'text': 'file warning', + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 2, + \ 'col': 3, + \ 'text': 'file error', + \ 'type': 'E', + \ }, + \], GetSimplerLoclist()