From 8216ee4a65872250f22c30aee6274ae5b23edf05 Mon Sep 17 00:00:00 2001 From: w0rp Date: Tue, 18 Mar 2025 09:56:07 +0000 Subject: [PATCH] Handle other LSP capabilities via ALE Save capabilities from language servers ALE connects to via Neovim's LSP client and handle responses to requests ALE sends to language servers. This enables ALE to work with Neovim's language client for its commands while also letting users directly use the connected clients for native Neovim keybinds and so on. --- autoload/ale/lsp.vim | 66 +++++++++++++++++++++++++++++--------------- lua/ale/lsp.lua | 32 +++++++++++++++++---- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index b66e93d0..903a5e14 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -195,101 +195,107 @@ endfunction " Update capabilities from the server, so we know which features the server " supports. -function! s:UpdateCapabilities(conn, capabilities) abort +function! ale#lsp#UpdateCapabilities(conn_id, capabilities) abort + let l:conn = get(s:connections, a:conn_id, {}) + + if empty(l:conn) + return + endif + if type(a:capabilities) isnot v:t_dict return endif if get(a:capabilities, 'hoverProvider') is v:true - let a:conn.capabilities.hover = 1 + let l:conn.capabilities.hover = 1 endif if type(get(a:capabilities, 'hoverProvider')) is v:t_dict - let a:conn.capabilities.hover = 1 + let l:conn.capabilities.hover = 1 endif if get(a:capabilities, 'referencesProvider') is v:true - let a:conn.capabilities.references = 1 + let l:conn.capabilities.references = 1 endif if type(get(a:capabilities, 'referencesProvider')) is v:t_dict - let a:conn.capabilities.references = 1 + let l:conn.capabilities.references = 1 endif if get(a:capabilities, 'renameProvider') is v:true - let a:conn.capabilities.rename = 1 + let l:conn.capabilities.rename = 1 endif if type(get(a:capabilities, 'renameProvider')) is v:t_dict - let a:conn.capabilities.rename = 1 + let l:conn.capabilities.rename = 1 endif if get(a:capabilities, 'codeActionProvider') is v:true - let a:conn.capabilities.code_actions = 1 + let l:conn.capabilities.code_actions = 1 endif if type(get(a:capabilities, 'codeActionProvider')) is v:t_dict - let a:conn.capabilities.code_actions = 1 + let l:conn.capabilities.code_actions = 1 endif if !empty(get(a:capabilities, 'completionProvider')) - let a:conn.capabilities.completion = 1 + let l:conn.capabilities.completion = 1 endif if type(get(a:capabilities, 'completionProvider')) is v:t_dict let l:chars = get(a:capabilities.completionProvider, 'triggerCharacters') if type(l:chars) is v:t_list - let a:conn.capabilities.completion_trigger_characters = l:chars + let l:conn.capabilities.completion_trigger_characters = l:chars endif endif if get(a:capabilities, 'definitionProvider') is v:true - let a:conn.capabilities.definition = 1 + let l:conn.capabilities.definition = 1 endif if type(get(a:capabilities, 'definitionProvider')) is v:t_dict - let a:conn.capabilities.definition = 1 + let l:conn.capabilities.definition = 1 endif if get(a:capabilities, 'typeDefinitionProvider') is v:true - let a:conn.capabilities.typeDefinition = 1 + let l:conn.capabilities.typeDefinition = 1 endif if type(get(a:capabilities, 'typeDefinitionProvider')) is v:t_dict - let a:conn.capabilities.typeDefinition = 1 + let l:conn.capabilities.typeDefinition = 1 endif if get(a:capabilities, 'implementationProvider') is v:true - let a:conn.capabilities.implementation = 1 + let l:conn.capabilities.implementation = 1 endif if type(get(a:capabilities, 'implementationProvider')) is v:t_dict - let a:conn.capabilities.implementation = 1 + let l:conn.capabilities.implementation = 1 endif if get(a:capabilities, 'workspaceSymbolProvider') is v:true - let a:conn.capabilities.symbol_search = 1 + let l:conn.capabilities.symbol_search = 1 endif if type(get(a:capabilities, 'workspaceSymbolProvider')) is v:t_dict - let a:conn.capabilities.symbol_search = 1 + let l:conn.capabilities.symbol_search = 1 endif if type(get(a:capabilities, 'textDocumentSync')) is v:t_dict let l:syncOptions = get(a:capabilities, 'textDocumentSync') if get(l:syncOptions, 'save') is v:true - let a:conn.capabilities.did_save = 1 + let l:conn.capabilities.did_save = 1 endif if type(get(l:syncOptions, 'save')) is v:t_dict - let a:conn.capabilities.did_save = 1 + let l:conn.capabilities.did_save = 1 let l:saveOptions = get(l:syncOptions, 'save') if get(l:saveOptions, 'includeText') is v:true - let a:conn.capabilities.includeText = 1 + let l:conn.capabilities.includeText = 1 endif endif endif @@ -334,7 +340,7 @@ function! ale#lsp#HandleInitResponse(conn, response) abort let a:conn.initialized = 1 elseif type(get(a:response, 'result')) is v:t_dict \&& has_key(a:response.result, 'capabilities') - call s:UpdateCapabilities(a:conn, a:response.result.capabilities) + call ale#lsp#UpdateCapabilities(a:conn.id, a:response.result.capabilities) let a:conn.initialized = 1 endif @@ -385,6 +391,20 @@ function! ale#lsp#HandleMessage(conn_id, message) abort endif endfunction +" Handle a JSON response from a language server. +" This is called from Lua for integration with Neovim's LSP API. +function! ale#lsp#HandleResponse(conn_id, response) abort + let l:conn = get(s:connections, a:conn_id, {}) + + if empty(l:conn) + return + endif + + for l:Callback in l:conn.callback_list + call ale#util#GetFunction(l:Callback)(a:conn_id, a:response) + endfor +endfunction + " Given a connection ID, mark it as a tsserver connection, so it will be " handled that way. function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort diff --git a/lua/ale/lsp.lua b/lua/ale/lsp.lua index b525ad64..2c58ec72 100644 --- a/lua/ale/lsp.lua +++ b/lua/ale/lsp.lua @@ -20,7 +20,15 @@ module.start = function(config) end } - config.on_init = function(_, _) + config.on_init = function(client, _) + -- Tell ALE about server capabilities as soon as we can. + -- This will inform ALE commands what can be done with each server, + -- such as "go to definition" support, etc. + vim.fn["ale#lsp#UpdateCapabilities"]( + config.name, + client.server_capabilities + ) + -- Neovim calls `on_init` before marking a client as active, meaning -- we can't get a client via get_client_by_id until after `on_init` is -- called. By deferring execution of calling the init callbacks we @@ -55,6 +63,7 @@ module.send_message = function(args) local client = vim.lsp.get_client_by_id(args.client_id) if args.is_notification then + -- For notifications we send a request and expect no direct response. local success = client.notify(args.method, args.params) if success then @@ -64,10 +73,23 @@ module.send_message = function(args) return 0 end - -- NOTE: We aren't yet handling reponses to requests properly! - -- NOTE: There is a fourth argument for a bufnr here, and it's not - -- clear what that argument is for or why we need it. - local success, request_id = client.request(args.method, args.params) + local success, request_id + + -- For request we send a request and handle the response. + -- + -- We set the bufnr to -1 to prevent Neovim from flushing anything, as ALE + -- already flushes changes to files before sending requests. + success, request_id = client.request( + args.method, + args.params, + function(_, result, _, _) + vim.fn["ale#lsp#HandleResponse"](client.name, { + id = request_id, + result = result, + }) + end, + -1 + ) if success then return request_id