Close #4458 - Add an ALEStopLSP command

Add an ALEStopLSP command to stop all language servers that match a
given name. Completions are available for the command. This makes it
possible to keep other language servers running other than the one
you're interested in stopping.
This commit is contained in:
w0rp 2023-09-16 17:03:02 +01:00
parent be69af2705
commit 1799f8bec6
No known key found for this signature in database
GPG Key ID: 0FC1ECAA8C81CD83
5 changed files with 216 additions and 46 deletions

View File

@ -1,9 +1,16 @@
" Author: w0rp <dev@w0rp.com>
" 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

View File

@ -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

View File

@ -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*

View File

@ -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(<f-args>, '<bang>')
" A command for linting manually.
command! -bar ALELint :call ale#Queue(0, 'lint_file')

View File

@ -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)