From 3dfebe45c610a40af55ba6b3260ab78bb221f54d Mon Sep 17 00:00:00 2001 From: w0rp Date: Wed, 19 Mar 2025 18:39:05 +0000 Subject: [PATCH] Add Neovim TCP connections to language servers Support TCP connections to language servers through Neovim's built in client. In all but what is currently the nightly builds of Neovim connections via a hostname will fail, but connections via an IP address should function. We will still enable the built in Neovim client by default anyway, as LSP clients very rarely connect over TCP. --- autoload/ale/lsp.vim | 34 +++++++++++++++++++++++++-- doc/ale.txt | 5 ++++ lua/ale/lsp.lua | 25 ++++++++++++++++++++ test/lsp/test_lsp_address_split.vader | 9 +++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 test/lsp/test_lsp_address_split.vader diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index fcc16069..932465ee 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -569,6 +569,21 @@ function! ale#lsp#StartProgram(conn_id, executable, command) abort return l:job_id > 0 endfunction +" Split an address into [host, port]. +" The port will either be a number or v:null. +function! ale#lsp#SplitAddress(address) abort + let l:port_match = matchlist(a:address, '\v:(\d+)$') + + if !empty(l:port_match) + let l:host = a:address[:-len(l:port_match[1]) - 2] + let l:port = l:port_match[1] + 0 + + return [l:host, l:port ? l:port : v:null] + endif + + return [a:address, v:null] +endfunction + " Connect to an LSP server via TCP. " " 1 will be returned if the connection is running, or 0 if the connection could @@ -577,8 +592,23 @@ function! ale#lsp#ConnectToAddress(conn_id, address) abort let l:conn = s:connections[a:conn_id] let l:started = 0 - " TODO: Start Neovim client here. - if !has_key(l:conn, 'channel_id') || !ale#socket#IsOpen(l:conn.channel_id) + if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver + let [l:host, l:port] = ale#lsp#SplitAddress(a:address) + + let l:client_id = luaeval('require("ale.lsp").start(_A)', { + \ 'name': a:conn_id, + \ 'host': l:host, + \ 'port': l:port, + \ 'root_dir': l:conn.root, + \ 'init_options': l:conn.init_options, + \}) + + if l:client_id > 0 + let l:conn.client_id = l:client_id + endif + + return l:client_id > 0 + elseif !has_key(l:conn, 'channel_id') || !ale#socket#IsOpen(l:conn.channel_id) let l:channel_id = ale#socket#Open(a:address, { \ 'callback': {_, mess -> ale#lsp#HandleMessage(a:conn_id, mess)}, \}) diff --git a/doc/ale.txt b/doc/ale.txt index 1d0d5a66..222c48f8 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -476,6 +476,11 @@ functionality for Neovim's LSP client should work as expected, and this ensures ALE integrates well with other plugins that rely on Neovim's LSP client. +NOTE: Neovim versions below `0.11.0` do not support socket connections to +language servers when the `address` defined in ALE uses a hostname instead of +an IP address. To work around this, configure language clients with an IP +address instead of a hostname, or revert back to ALE's custom LSP client. + See |lsp| for information on Neovim's built in LSP client. For diagnostics, for computing problems to show via ALE, ALE overrides the diff --git a/lua/ale/lsp.lua b/lua/ale/lsp.lua index f45f4598..01c59c86 100644 --- a/lua/ale/lsp.lua +++ b/lua/ale/lsp.lua @@ -6,6 +6,31 @@ module.start = function(config) config.init_options[true] = nil end + -- If configuring LSP via a socket connection, then generate the cmd + -- using vim.lsp.rpc.connect(), as defined in Neovim documentation. + if config.host then + local cmd_func = vim.lsp.rpc.connect(config.host, config.port) + config.host = nil + config.port = nil + + -- Wrap the cmd function so we don't throw errors back to the user + -- if the connection to an address fails to start. + -- + -- We will separately log in ALE that we failed to start a connection. + -- + -- In older Neovim versions TCP connections do not function if supplied + -- a hostname instead of an address. + config.cmd = function(dispatch) + local success, result = pcall(cmd_func, dispatch) + + if success then + return result + end + + return nil + end + end + config.handlers = { -- Override Neovim's handling of diagnostics to run through ALE's -- functions so all of the functionality in ALE works. diff --git a/test/lsp/test_lsp_address_split.vader b/test/lsp/test_lsp_address_split.vader new file mode 100644 index 00000000..adc1735a --- /dev/null +++ b/test/lsp/test_lsp_address_split.vader @@ -0,0 +1,9 @@ +Execute(Address splitting should function as intended): + AssertEqual ['foo', v:null], ale#lsp#SplitAddress('foo') + AssertEqual ['foo:', v:null], ale#lsp#SplitAddress('foo:') + AssertEqual ['foo', v:null], ale#lsp#SplitAddress('foo:0') + AssertEqual ['foo', 123], ale#lsp#SplitAddress('foo:123') + AssertEqual ['protocol:/foo:', v:null], ale#lsp#SplitAddress('protocol:/foo:') + AssertEqual ['protocol:/foo', v:null], ale#lsp#SplitAddress('protocol:/foo:0') + AssertEqual ['protocol:/foo', 123], ale#lsp#SplitAddress('protocol:/foo:123') + AssertEqual ['protocol:foo', v:null], ale#lsp#SplitAddress('protocol:foo')