diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 944aab3..3049ab5 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -33,17 +33,12 @@ function! ale#engine#InitBufferInfo(buffer) abort if !has_key(g:ale_buffer_info, a:buffer) " job_list will hold the list of jobs " loclist holds the loclist items after all jobs have completed. - " lint_file_loclist holds items from the last run including linters - " which use the lint_file option. - " new_loclist holds loclist items while jobs are being run. " temporary_file_list holds temporary files to be cleaned up " temporary_directory_list holds temporary directories to be cleaned up " history holds a list of previously run commands for this buffer let g:ale_buffer_info[a:buffer] = { \ 'job_list': [], \ 'loclist': [], - \ 'lint_file_loclist': [], - \ 'new_loclist': [], \ 'temporary_file_list': [], \ 'temporary_directory_list': [], \ 'history': [], @@ -154,53 +149,66 @@ function! s:HandleExit(job_id, exit_code) abort " to set default values for loclist items. let l:linter_loclist = ale#engine#FixLocList(l:buffer, l:linter, l:linter_loclist) - " Add the loclist items from the linter. - " 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. - return - endif - - " Automatically remove all managed temporary files and directories - " 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 + " Remove previous items for this linter. + call filter(g:ale_buffer_info[l:buffer].loclist, 'v:val.linter_name !=# l:linter.name') + " Add the new items. + call extend(g:ale_buffer_info[l:buffer].loclist, l:linter_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(l:combined_list, 'ale#util#LocItemCompare') + call sort(g:ale_buffer_info[l:buffer].loclist, '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 = l:combined_list - let g:ale_buffer_info[l:buffer].new_loclist = [] + let l:linting_is_done = empty(g:ale_buffer_info[l:buffer].job_list) + + if l:linting_is_done + " Automatically remove all managed temporary files and directories + " now that all jobs have completed. + call ale#engine#RemoveManagedFiles(l:buffer) + + " Figure out which linters are still enabled, and remove + " problems for linters which are no longer enabled. + let l:name_map = {} + + for l:linter in ale#linter#Get(getbufvar(l:buffer, '&filetype')) + let l:name_map[l:linter.name] = 1 + endfor + + call filter( + \ g:ale_buffer_info[l:buffer].loclist, + \ 'get(l:name_map, v:val.linter_name)', + \) + endif call ale#engine#SetResults(l:buffer, g:ale_buffer_info[l:buffer].loclist) - " Call user autocommands. This allows users to hook into ALE's lint cycle. - silent doautocmd User ALELint + if l:linting_is_done + " Call user autocommands. This allows users to hook into ALE's lint cycle. + silent doautocmd User ALELint + endif endfunction function! ale#engine#SetResults(buffer, loclist) abort + let l:info = get(g:ale_buffer_info, a:buffer, {}) + let l:job_list = get(l:info, 'job_list', []) + let l:linting_is_done = empty(l:job_list) + " 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) + + if l:linting_is_done + call ale#sign#RemoveDummySignIfNeeded(a:buffer) + endif endif if g:ale_set_quickfix || g:ale_set_loclist call ale#list#SetLists(a:buffer, a:loclist) + + if l:linting_is_done + call ale#list#CloseWindowIfNeeded(a:buffer) + endif endif if exists('*ale#statusline#Update') diff --git a/autoload/ale/list.vim b/autoload/ale/list.vim index f446bba..75b4ef3 100644 --- a/autoload/ale/list.vim +++ b/autoload/ale/list.vim @@ -35,13 +35,8 @@ function! ale#list#SetLists(buffer, loclist) abort endif endif - " If we don't auto-open lists, bail out here. - if !g:ale_open_list && !g:ale_keep_list_window_open - return - endif - " If we have errors in our list, open the list. Only if it isn't already open - if len(a:loclist) > 0 || g:ale_keep_list_window_open + if (g:ale_open_list && !empty(a:loclist)) || g:ale_keep_list_window_open let l:winnr = winnr() if g:ale_set_quickfix @@ -56,13 +51,21 @@ function! ale#list#SetLists(buffer, loclist) abort if l:winnr !=# winnr() wincmd p endif + endif +endfunction - " Only close if the list is totally empty (relying on Vim's state, not our - " own). This keeps us from closing the window when other plugins have - " populated it. - elseif !g:ale_keep_list_window_open && g:ale_set_quickfix && len(getqflist()) == 0 - cclose - elseif !g:ale_keep_list_window_open && len(getloclist(0)) == 0 +function! ale#list#CloseWindowIfNeeded(buffer) abort + if g:ale_keep_list_window_open || !g:ale_open_list + return + endif + + " Only close windows if the quickfix list or loclist is completely empty, + " including errors set through other means. + if g:ale_set_quickfix + if empty(getqflist()) + cclose + endif + elseif g:ale_set_loclist && empty(getloclist(0)) lclose endif endfunction diff --git a/autoload/ale/sign.vim b/autoload/ale/sign.vim index 687d567..f4ebed8 100644 --- a/autoload/ale/sign.vim +++ b/autoload/ale/sign.vim @@ -118,7 +118,20 @@ function! s:GroupLoclistItems(loclist) abort let l:last_lnum = l:obj.lnum endfor - return l:grouped_items + " Now we've gathered the items in groups, filter the groups down to + " the groups containing at least one new item. + let l:new_grouped_items = [] + + for l:group in l:grouped_items + for l:obj in l:group + if !has_key(l:obj, 'sign_id') + call add(l:new_grouped_items, l:group) + break + endif + endfor + endfor + + return l:new_grouped_items endfunction function! s:IsDummySignSet(current_id_list) abort @@ -189,14 +202,18 @@ function! ale#sign#SetSignColumnHighlight(has_problems) abort endif endfunction -function! s:PlaceNewSigns(buffer, grouped_items) abort +function! s:PlaceNewSigns(buffer, grouped_items, current_sign_offset) abort if g:ale_change_sign_column_color call ale#sign#SetSignColumnHighlight(!empty(a:grouped_items)) endif + let l:offset = a:current_sign_offset > 0 + \ ? a:current_sign_offset + \ : g:ale_sign_offset + " Add the new signs, for l:index in range(0, len(a:grouped_items) - 1) - let l:sign_id = l:index + g:ale_sign_offset + 1 + let l:sign_id = l:offset + l:index + 1 let l:sublist = a:grouped_items[l:index] let l:type = ale#sign#GetSignType(a:grouped_items[l:index]) @@ -232,22 +249,17 @@ endfunction " Given some current signs and a loclist, look for items with sign IDs, " and change the line numbers for loclist items to match the signs. -function! s:UpdateLineNumbers(current_sign_list, loclist) abort - let l:items_by_sign_id = s:GetItemsWithSignIDs(a:loclist) - +function! s:UpdateLineNumbers(current_sign_list, items_by_sign_id) abort " Do nothing if there's nothing to work with. - if empty(l:items_by_sign_id) + if empty(a:items_by_sign_id) return endif for [l:line, l:sign_id, l:name] in a:current_sign_list - for l:obj in get(l:items_by_sign_id, l:sign_id, []) + for l:obj in get(a:items_by_sign_id, l:sign_id, []) let l:obj.lnum = l:line endfor endfor - - " Sort items again. - call sort(a:loclist, 'ale#util#LocItemCompare') endfunction " This function will set the signs which show up on the left. @@ -260,8 +272,13 @@ function! ale#sign#SetSigns(buffer, loclist) abort " Find the current markers let l:current_sign_list = ale#sign#FindCurrentSigns(a:buffer) + " Get a mapping from sign IDs to current loclist items which have them. + let l:items_by_sign_id = s:GetItemsWithSignIDs(a:loclist) - call s:UpdateLineNumbers(l:current_sign_list, a:loclist) + " Use sign information to update the line numbers for the loclist items. + call s:UpdateLineNumbers(l:current_sign_list, l:items_by_sign_id) + " Sort items again, as the line numbers could have changed. + call sort(a:loclist, 'ale#util#LocItemCompare') let l:grouped_items = s:GroupLoclistItems(a:loclist) @@ -277,16 +294,19 @@ function! ale#sign#SetSigns(buffer, loclist) abort " while we add the new signs, if we had signs before. for [l:line, l:sign_id, l:name] in l:current_sign_list if l:sign_id != g:ale_sign_offset + \&& !has_key(l:items_by_sign_id, l:sign_id) exec 'sign unplace ' . l:sign_id . ' buffer=' . a:buffer endif endfor - call s:PlaceNewSigns(a:buffer, l:grouped_items) + " Compute a sign ID offset so we don't hit the same sign IDs again. + let l:current_sign_offset = max(map(keys(l:items_by_sign_id), 'str2nr(v:val)')) - " Remove the dummy sign now we've updated the signs, unless we want - " to keep it, which will keep the sign column open even when there are - " no warnings or errors. - if l:is_dummy_sign_set && !g:ale_sign_column_always + call s:PlaceNewSigns(a:buffer, l:grouped_items, l:current_sign_offset) +endfunction + +function! ale#sign#RemoveDummySignIfNeeded(buffer) abort + if !g:ale_sign_column_always execute 'sign unplace ' . g:ale_sign_offset . ' buffer=' . a:buffer endif endfunction diff --git a/test/sign/test_sign_placement.vader b/test/sign/test_sign_placement.vader index f8e926b..77f9bb6 100644 --- a/test/sign/test_sign_placement.vader +++ b/test/sign/test_sign_placement.vader @@ -126,30 +126,32 @@ Execute(The current signs should be set for running a job): Execute(Loclist items with sign_id values should be kept): - exec 'sign place 1000347 line=15 name=ALEErrorSign buffer=' . bufnr('%') - exec 'sign place 1000348 line=16 name=ALEWarningSign buffer=' . bufnr('%') + exec 'sign place 1000347 line=3 name=ALEErrorSign buffer=' . bufnr('%') + exec 'sign place 1000348 line=15 name=ALEErrorSign buffer=' . bufnr('%') + exec 'sign place 1000349 line=16 name=ALEWarningSign buffer=' . bufnr('%') let g:loclist = [ - \ {'lnum': 1, 'col': 1, 'type': 'E', 'text': 'a', 'sign_id': 1000347}, - \ {'lnum': 2, 'col': 1, 'type': 'W', 'text': 'b', 'sign_id': 1000348}, - \ {'lnum': 3, 'col': 1, 'type': 'E', 'text': 'c'}, + \ {'lnum': 1, 'col': 1, 'type': 'E', 'text': 'a', 'sign_id': 1000348}, + \ {'lnum': 2, 'col': 1, 'type': 'W', 'text': 'b', 'sign_id': 1000349}, + \ {'lnum': 3, 'col': 1, 'type': 'E', 'text': 'c', 'sign_id': 1000347}, \ {'lnum': 4, 'col': 1, 'type': 'W', 'text': 'd'}, \ {'lnum': 15, 'col': 2, 'type': 'W', 'text': 'e'}, \ {'lnum': 16, 'col': 2, 'type': 'E', 'text': 'f'}, \] call ale#sign#SetSigns(bufnr('%'), g:loclist) + call ale#sign#RemoveDummySignIfNeeded(bufnr('%')) - " Line numbers should be changed, sign_id values should be replaced, - " and items should be sorted again. + " Sign IDs from before should be kept, and new signs should use + " IDs that haven't been used yet. AssertEqual \ [ - \ {'lnum': 3, 'col': 1, 'type': 'E', 'text': 'c', 'sign_id': 1000001}, - \ {'lnum': 4, 'col': 1, 'type': 'W', 'text': 'd', 'sign_id': 1000002}, - \ {'lnum': 15, 'col': 1, 'type': 'E', 'text': 'a', 'sign_id': 1000003}, - \ {'lnum': 15, 'col': 2, 'type': 'W', 'text': 'e', 'sign_id': 1000003}, - \ {'lnum': 16, 'col': 1, 'type': 'W', 'text': 'b', 'sign_id': 1000004}, - \ {'lnum': 16, 'col': 2, 'type': 'E', 'text': 'f', 'sign_id': 1000004}, + \ {'lnum': 3, 'col': 1, 'type': 'E', 'text': 'c', 'sign_id': 1000347}, + \ {'lnum': 4, 'col': 1, 'type': 'W', 'text': 'd', 'sign_id': 1000350}, + \ {'lnum': 15, 'col': 1, 'type': 'E', 'text': 'a', 'sign_id': 1000351}, + \ {'lnum': 15, 'col': 2, 'type': 'W', 'text': 'e', 'sign_id': 1000351}, + \ {'lnum': 16, 'col': 1, 'type': 'W', 'text': 'b', 'sign_id': 1000352}, + \ {'lnum': 16, 'col': 2, 'type': 'E', 'text': 'f', 'sign_id': 1000352}, \ ], \ g:loclist @@ -158,12 +160,14 @@ Execute(Loclist items with sign_id values should be kept): " now have new warnings. AssertEqual \ [ - \ ['3', '1000001', 'ALEErrorSign'], - \ ['4', '1000002', 'ALEWarningSign'], - \ ['15', '1000003', 'ALEErrorSign'], - \ ['16', '1000004', 'ALEErrorSign'], + \ ['15', '1000348', 'ALEErrorSign'], + \ ['15', '1000351', 'ALEErrorSign'], + \ ['16', '1000349', 'ALEWarningSign'], + \ ['16', '1000352', 'ALEErrorSign'], + \ ['3', '1000347', 'ALEErrorSign'], + \ ['4', '1000350', 'ALEWarningSign'], \ ], - \ ParseSigns() + \ sort(ParseSigns()) Execute(No excpetions should be thrown when setting signs for invalid buffers): call ale#sign#SetSigns(123456789, [{'lnum': 15, 'col': 2, 'type': 'W', 'text': 'e'}]) diff --git a/test/smoke_test.vader b/test/smoke_test.vader index 30f3253..209b5bb 100644 --- a/test/smoke_test.vader +++ b/test/smoke_test.vader @@ -1,13 +1,16 @@ Before: function! TestCallback(buffer, output) return [{ - \ 'bufnr': a:buffer, \ 'lnum': 2, - \ 'vcol': 0, \ 'col': 3, \ 'text': a:output[0], - \ 'type': 'E', - \ 'nr': -1, + \}] + endfunction + function! TestCallback2(buffer, output) + return [{ + \ 'lnum': 3, + \ 'col': 4, + \ 'text': a:output[0], \}] endfunction @@ -22,6 +25,7 @@ Before: After: let g:ale_buffer_info = {} delfunction TestCallback + delfunction TestCallback2 call ale#linter#Reset() Given foobar (Some imaginary filetype): @@ -46,3 +50,31 @@ Execute(Linters should run with the default options): \ 'pattern': '', \ 'valid': 1, \ }], getloclist(0) + +Execute(Previous errors should be removed when linters change): + call ale#Lint() + call ale#engine#WaitForJobs(2000) + + call ale#linter#Reset() + + call ale#linter#Define('foobar', { + \ 'name': 'testlinter2', + \ 'callback': 'TestCallback2', + \ 'executable': 'echo', + \ 'command': '/bin/sh -c ''echo baz boz''', + \}) + + call ale#Lint() + call ale#engine#WaitForJobs(2000) + + AssertEqual [{ + \ 'bufnr': bufnr('%'), + \ 'lnum': 3, + \ 'vcol': 0, + \ 'col': 4, + \ 'text': 'baz boz', + \ 'type': 'E', + \ 'nr': -1, + \ 'pattern': '', + \ 'valid': 1, + \ }], getloclist(0) diff --git a/test/test_list_opening.vader b/test/test_list_opening.vader index 89b1416..a46f28e 100644 --- a/test/test_list_opening.vader +++ b/test/test_list_opening.vader @@ -64,14 +64,17 @@ Execute(The quickfix window should open for just the loclist): " It should not open for an empty list. call ale#list#SetLists(bufnr('%'), []) + call ale#list#CloseWindowIfNeeded(bufnr('')) Assert !ale#list#IsQuickfixOpen() " With a non-empty loclist, the window must open. call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) Assert ale#list#IsQuickfixOpen() " Clear the list and it should close again. call ale#list#SetLists(bufnr('%'), []) + call ale#list#CloseWindowIfNeeded(bufnr('')) Assert !ale#list#IsQuickfixOpen() Execute(The quickfix window height should be correct for the loclist): @@ -79,6 +82,7 @@ Execute(The quickfix window height should be correct for the loclist): let g:ale_list_window_size = 7 call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) AssertEqual 7, GetQuickfixHeight() @@ -87,6 +91,7 @@ Execute(The quickfix window height should be correct for the loclist with buffer let b:ale_list_window_size = 8 call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) AssertEqual 8, GetQuickfixHeight() @@ -96,13 +101,16 @@ Execute(The quickfix window should stay open for just the loclist): " The window should stay open after even after it is made blank again. call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) call ale#list#SetLists(bufnr('%'), []) + call ale#list#CloseWindowIfNeeded(bufnr('')) Assert ale#list#IsQuickfixOpen() Execute(The quickfix window should not open by default when quickfix is on): let g:ale_set_quickfix = 1 call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) Assert !ale#list#IsQuickfixOpen() Execute(The quickfix window should open for the quickfix list): @@ -111,15 +119,18 @@ Execute(The quickfix window should open for the quickfix list): " It should not open for an empty list. call ale#list#SetLists(bufnr('%'), []) - Assert !ale#list#IsQuickfixOpen() + call ale#list#CloseWindowIfNeeded(bufnr('')) + Assert !ale#list#IsQuickfixOpen(), 'The quickfix window was opened when the list was empty' " With a non-empty quickfix list, the window must open. call ale#list#SetLists(bufnr('%'), g:loclist) - Assert ale#list#IsQuickfixOpen() + call ale#list#CloseWindowIfNeeded(bufnr('')) + Assert ale#list#IsQuickfixOpen(), 'The quickfix window was closed when the list was not empty' " Clear the list and it should close again. call ale#list#SetLists(bufnr('%'), []) - Assert !ale#list#IsQuickfixOpen() + call ale#list#CloseWindowIfNeeded(bufnr('')) + Assert !ale#list#IsQuickfixOpen(), 'The quickfix window was not closed when the list was empty' Execute(The quickfix window should stay open for the quickfix list): let g:ale_set_quickfix = 1 @@ -128,7 +139,9 @@ Execute(The quickfix window should stay open for the quickfix list): " The window should stay open after even after it is made blank again. call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) call ale#list#SetLists(bufnr('%'), []) + call ale#list#CloseWindowIfNeeded(bufnr('')) Assert ale#list#IsQuickfixOpen() Execute(The quickfix window height should be correct for the quickfix list): @@ -137,6 +150,7 @@ Execute(The quickfix window height should be correct for the quickfix list): let g:ale_list_window_size = 7 call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) AssertEqual 7, GetQuickfixHeight() @@ -146,5 +160,6 @@ Execute(The quickfix window height should be correct for the quickfix list with let b:ale_list_window_size = 8 call ale#list#SetLists(bufnr('%'), g:loclist) + call ale#list#CloseWindowIfNeeded(bufnr('')) AssertEqual 8, GetQuickfixHeight()