diff --git a/autoload/ale/lsp/reset.vim b/autoload/ale/lsp/reset.vim index 85188b5a..1801db01 100644 --- a/autoload/ale/lsp/reset.vim +++ b/autoload/ale/lsp/reset.vim @@ -1,9 +1,16 @@ +" Author: w0rp +" Description: Functions for resetting LSP servers. + +function! s:Message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + " Stop all LSPs and remove all of the data for them. function! ale#lsp#reset#StopAllLSPs() abort call ale#lsp#StopAll() if exists('*ale#definition#ClearLSPData') - " Clear the mapping for connections, etc. + " Clear the go to definition mapping for everything. call ale#definition#ClearLSPData() endif @@ -25,3 +32,61 @@ function! ale#lsp#reset#StopAllLSPs() abort endfor endif endfunction + +function! ale#lsp#reset#Complete(arg, line, pos) abort + let l:linter_map = ale#lsp_linter#GetLSPLinterMap() + let l:candidates = map(values(l:linter_map), {_, linter -> linter.name}) + call uniq(sort(l:candidates)) + call filter(l:candidates, {_, name -> name =~? a:arg}) + + return l:candidates +endfunction + +function! ale#lsp#reset#StopLSP(name, bang) abort + let l:linter_map = ale#lsp_linter#GetLSPLinterMap() + let l:matched = filter( + \ items(l:linter_map), + \ {_, item -> item[1].name is# a:name} + \) + + if empty(l:matched) + if a:bang isnot# '!' + call s:Message('No running language server with name: ' . a:name) + endif + + return + endif + + " Stop LSP connections first. + for [l:conn_id, l:linter] in l:matched + call ale#lsp#Stop(l:conn_id) + endfor + + if exists('*ale#definition#ClearLSPData') + " Clear the go to definition mapping for everything. + call ale#definition#ClearLSPData() + endif + + " Remove connections from the lsp_linter map. + for [l:conn_id, l:linter] in l:matched + call remove(l:linter_map, l:conn_id) + endfor + + " Remove the problems for the LSP linters in every buffer. + for [l:buffer_string, l:info] in items(g:ale_buffer_info) + let l:buffer = str2nr(l:buffer_string) + let l:should_clear_buffer = 0 + + for l:item in l:info.loclist + if l:item.linter_name is# a:name + let l:should_clear_buffer = 1 + + break + endif + endfor + + if l:should_clear_buffer + call ale#engine#HandleLoclist(a:name, l:buffer, [], 0) + endif + endfor +endfunction diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index 05a0294c..a3c8b24a 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -8,13 +8,9 @@ if !has_key(s:, 'lsp_linter_map') let s:lsp_linter_map = {} endif -" A Dictionary to track one-shot handlers for custom LSP requests -let s:custom_handlers_map = get(s:, 'custom_handlers_map', {}) - " Clear LSP linter data for the linting engine. function! ale#lsp_linter#ClearLSPData() abort let s:lsp_linter_map = {} - let s:custom_handlers_map = {} endfunction " Only for internal use. @@ -82,7 +78,12 @@ function! s:ShouldIgnoreDiagnostics(buffer, linter) abort endfunction function! s:HandleLSPDiagnostics(conn_id, response) abort - let l:linter = s:lsp_linter_map[a:conn_id] + let l:linter = get(s:lsp_linter_map, a:conn_id) + + if empty(l:linter) + return + endif + let l:filename = ale#util#ToResource(a:response.params.uri) let l:escaped_name = escape( \ fnameescape(l:filename), @@ -540,9 +541,14 @@ endfunction function! s:HandleLSPResponseToCustomRequests(conn_id, response) abort if has_key(a:response, 'id') - \&& has_key(s:custom_handlers_map, a:response.id) - let l:Handler = remove(s:custom_handlers_map, a:response.id) - call l:Handler(a:response) + " Get the custom handlers Dictionary from the linter map. + let l:linter = get(s:lsp_linter_map, a:conn_id, {}) + let l:custom_handlers = get(l:linter, 'custom_handlers', {}) + + if has_key(l:custom_handlers, a:response.id) + let l:Handler = remove(l:custom_handlers, a:response.id) + call l:Handler(a:response) + endif endif endfunction @@ -553,7 +559,17 @@ function! s:OnReadyForCustomRequests(args, linter, lsp_details) abort if l:request_id > 0 && has_key(a:args, 'handler') let l:Callback = function('s:HandleLSPResponseToCustomRequests') call ale#lsp#RegisterCallback(l:id, l:Callback) - let s:custom_handlers_map[l:request_id] = a:args.handler + + " Remember the linter this connection is for. + let s:lsp_linter_map[l:id] = a:linter + + " Add custom_handlers to the linter Dictionary. + if !has_key(a:linter, 'custom_handlers') + let a:linter.custom_handlers = {} + endif + + " Put the handler function in the map to call later. + let a:linter.custom_handlers[l:request_id] = a:args.handler endif endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 211ac1c3..9b25e6ec 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -455,6 +455,11 @@ If you want to use another plugin for LSP features and tsserver, you can use the |g:ale_disable_lsp| setting to disable ALE's own LSP integrations, or ignore particular linters with |g:ale_linters_ignore|. +If for any reason you want to stop a language server ALE starts, such as when +a project configuration has significantly changed, or new files have been +added the language server isn't aware of, use either |ALEStopLSP| or +|ALEStopAllLSPs| to stop the server until ALE automatically starts it again. + ------------------------------------------------------------------------------- 5.1 Completion *ale-completion* @@ -3931,6 +3936,17 @@ ALEStopAllLSPs *ALEStopAllLSPs* This command can be used when LSP clients mess up and need to be restarted. +ALEStopLSP `linter_name` *ALEStopLSP* + + `ALEStopLSP` will stop a specific language server with a given linter name. + Completion is supported for currently running language servers. All language + servers with the given name will be stopped across all buffers for all + projects. + + If the command is run with a bang (`:ALEStopLSP!`), all warnings will be + suppressed. + + =============================================================================== 9. API *ale-api* diff --git a/plugin/ale.vim b/plugin/ale.vim index a721bd89..47934fa0 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -246,6 +246,8 @@ command! -bar ALEDisableBuffer :call ale#toggle#DisableBuffer(bufnr('')) command! -bar ALEResetBuffer :call ale#toggle#ResetBuffer(bufnr('')) " A command to stop all LSP-like clients, including tsserver. command! -bar ALEStopAllLSPs :call ale#lsp#reset#StopAllLSPs() +" A command to stop a specific language server, or tsseserver. +command! -bar -bang -nargs=1 -complete=customlist,ale#lsp#reset#Complete ALEStopLSP :call ale#lsp#reset#StopLSP(, '') " A command for linting manually. command! -bar ALELint :call ale#Queue(0, 'lint_file') diff --git a/test/lsp/test_reset_lsp.vader b/test/lsp/test_reset_lsp.vader index 310b3d62..c6be47e6 100644 --- a/test/lsp/test_reset_lsp.vader +++ b/test/lsp/test_reset_lsp.vader @@ -5,6 +5,7 @@ Before: Save g:ale_set_loclist Save g:ale_set_highlights Save g:ale_echo_cursor + Save g:ale_buffer_info let g:ale_enabled = 0 let g:ale_set_signs = 0 @@ -12,14 +13,20 @@ Before: let g:ale_set_loclist = 0 let g:ale_set_highlights = 0 let g:ale_echo_cursor = 0 + let g:expr_list = [] function EmptyString() abort return '' endfunction + runtime autoload/ale/util.vim + + function! ale#util#Execute(expr) abort + call add(g:expr_list, a:expr) + endfunction + call ale#engine#InitBufferInfo(bufnr('')) - " Call this function first, so we can be sure the module is loaded before we - " check if it exists. + " Call this function first, to clear LSP data. call ale#lsp_linter#ClearLSPData() call ale#linter#Define('testft', { @@ -30,7 +37,14 @@ Before: \ 'project_root': function('EmptyString'), \ 'language': function('EmptyString'), \}) - + call ale#linter#Define('testft', { + \ 'name': 'lsplinter2', + \ 'lsp': 'tsserver', + \ 'executable': function('EmptyString'), + \ 'command': function('EmptyString'), + \ 'project_root': function('EmptyString'), + \ 'language': function('EmptyString'), + \}) call ale#linter#Define('testft', { \ 'name': 'otherlinter', \ 'callback': 'TestCallback', @@ -42,34 +56,23 @@ Before: After: Restore + delfunction EmptyString + unlet! g:expr_list unlet! b:ale_save_event_fired - delfunction EmptyString + " Clear LSP data after tests. + call ale#lsp_linter#ClearLSPData() + + runtime autoload/ale/util.vim + call ale#linter#Reset() Given testft(Some file with an imaginary filetype): Execute(ALEStopAllLSPs should clear the loclist): + " For these tests we only need to set the keys we need. let g:ale_buffer_info[bufnr('')].loclist = [ - \ { - \ 'text': 'a', - \ 'lnum': 10, - \ 'col': 0, - \ 'bufnr': bufnr(''), - \ 'vcol': 0, - \ 'type': 'E', - \ 'nr': -1, - \ 'linter_name': 'lsplinter', - \ }, - \ { - \ 'text': 'a', - \ 'lnum': 10, - \ 'col': 0, - \ 'bufnr': bufnr(''), - \ 'vcol': 0, - \ 'type': 'E', - \ 'nr': -1, - \ 'linter_name': 'otherlinter', - \ }, + \ {'linter_name': 'lsplinter'}, + \ {'linter_name': 'otherlinter'}, \] let g:ale_buffer_info[bufnr('')].active_linter_list = [ \ {'name': 'lsplinter'}, @@ -79,20 +82,88 @@ Execute(ALEStopAllLSPs should clear the loclist): ALEStopAllLSPs " The loclist should be updated. - AssertEqual g:ale_buffer_info[bufnr('')].loclist, [ - \ { - \ 'text': 'a', - \ 'lnum': 10, - \ 'col': 0, - \ 'bufnr': bufnr(''), - \ 'vcol': 0, - \ 'type': 'E', - \ 'nr': -1, - \ 'linter_name': 'otherlinter', - \ }, - \] + AssertEqual + \ ['otherlinter'], + \ map(copy(g:ale_buffer_info[bufnr('')].loclist), 'v:val.linter_name') " The LSP linter should be removed from the active linter list. AssertEqual \ ['otherlinter'], \ map(copy(g:ale_buffer_info[bufnr('')].active_linter_list), 'v:val.name') + +Execute(ALEStopLSP should stop a named LSP): + let g:ale_buffer_info[bufnr('')].loclist = [ + \ {'linter_name': 'lsplinter'}, + \ {'linter_name': 'lsplinter2'}, + \ {'linter_name': 'otherlinter'}, + \] + let g:ale_buffer_info[bufnr('')].active_linter_list = [ + \ {'name': 'lsplinter'}, + \ {'name': 'lsplinter2'}, + \ {'name': 'otherlinter'}, + \] + call ale#lsp_linter#SetLSPLinterMap({ + \ 'conn1': {'name': 'lsplinter'}, + \ 'conn2': {'name': 'lsplinter2'}, + \ 'conn3': {'name': 'lsplinter'}, + \}) + + ALEStopLSP lsplinter + + " We should remove only the items for this linter. + AssertEqual + \ ['lsplinter2', 'otherlinter'], + \ map(copy(g:ale_buffer_info[bufnr('')].loclist), 'v:val.linter_name') + + " The linter should be removed from the active linter list. + AssertEqual + \ ['lsplinter2', 'otherlinter'], + \ map(copy(g:ale_buffer_info[bufnr('')].active_linter_list), 'v:val.name') + + " The connections linters with this name should be removed. + AssertEqual + \ {'conn2': {'name': 'lsplinter2'}}, + \ ale#lsp_linter#GetLSPLinterMap() + +Execute(ALEStopLSP should not clear results for linters not running): + let g:ale_buffer_info[bufnr('')].loclist = [ + \ {'linter_name': 'lsplinter'}, + \ {'linter_name': 'otherlinter'}, + \] + let g:ale_buffer_info[bufnr('')].active_linter_list = [ + \ {'name': 'lsplinter'}, + \ {'name': 'otherlinter'}, + \] + + ALEStopLSP lsplinter + + " We should emit a message saying the server isn't running. + AssertEqual + \ ['echom ''No running language server with name: lsplinter'''], + \ g:expr_list + + " We should keep the linter items. + AssertEqual + \ ['lsplinter', 'otherlinter'], + \ map(copy(g:ale_buffer_info[bufnr('')].loclist), 'v:val.linter_name') + AssertEqual + \ ['lsplinter', 'otherlinter'], + \ map(copy(g:ale_buffer_info[bufnr('')].active_linter_list), 'v:val.name') + +Execute(ALEStopLSP with a bang should not emit warnings): + ALEStopLSP! lsplinter + + AssertEqual [], g:expr_list + +Execute(ALEStopLSP's completion function should suggest running linter names): + call ale#lsp_linter#SetLSPLinterMap({ + \ 'conn1': {'name': 'pyright'}, + \ 'conn2': {'name': 'pylsp'}, + \ 'conn3': {'name': 'imaginaryserver'}, + \}) + + AssertEqual + \ ['imaginaryserver', 'pylsp', 'pyright'], + \ ale#lsp#reset#Complete('', '', 42) + AssertEqual ['imaginaryserver'], ale#lsp#reset#Complete('inary', '', 42) + AssertEqual ['pylsp'], ale#lsp#reset#Complete('LSP', '', 42)