Start up language servers with Neovim's API

Get language servers starting and displaying diagnostics with Neovim's
API in Neovim 0.8 and up. With this set up, now ALE needs to take over
handling diagnostics returned by the language servers.
This commit is contained in:
w0rp
2025-03-17 17:34:50 +00:00
parent f3512cd778
commit 8ee20eca4b
4 changed files with 142 additions and 8 deletions

View File

@@ -313,6 +313,21 @@ function! ale#lsp#UpdateConfig(conn_id, buffer, config) abort
return 1
endfunction
function! ale#lsp#CallInitCallbacks(conn_id) abort
let l:conn = s:connections[a:conn_id]
" Ensure the connection is marked as initialized.
" For integration with Neovim's LSP tooling this ensures immediately
" call OnInit functions in Vim after the `on_init` callback is called.
let l:conn.initialized = 1
" Call capabilities callbacks queued for the project.
for l:Callback in l:conn.init_queue
call l:Callback()
endfor
let l:conn.init_queue = []
endfunction
function! ale#lsp#HandleInitResponse(conn, response) abort
if get(a:response, 'method', '') is# 'initialize'
@@ -331,12 +346,7 @@ function! ale#lsp#HandleInitResponse(conn, response) abort
" The initialized message must be sent before everything else.
call ale#lsp#Send(a:conn.id, ale#lsp#message#Initialized())
" Call capabilities callbacks queued for the project.
for l:Callback in a:conn.init_queue
call l:Callback()
endfor
let a:conn.init_queue = []
call ale#lsp#CallInitCallbacks(a:conn.id)
endfunction
function! ale#lsp#HandleMessage(conn_id, message) abort
@@ -482,6 +492,29 @@ function! ale#lsp#StartProgram(conn_id, executable, command) abort
let l:conn = s:connections[a:conn_id]
let l:started = 0
if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver
" For Windows from 'cmd /s/c "foo bar"' we need 'foo bar'
let l:lsp_cmd = has('win32')
\ ? ['cmd', '/s/c/', a:command[10:-2]]
\ : a:command
" Always call lsp.start, which will either create or re-use a
" connection. We'll set `attach` to `false` so we can later use
" our OpenDocument function to attach the buffer separately.
let l:client_id = luaeval('require("ale.lsp").start(_A)', {
\ 'name': a:conn_id,
\ 'cmd': l:lsp_cmd,
\ '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
endif
if !has_key(l:conn, 'job_id') || !ale#job#HasOpenChannel(l:conn.job_id)
let l:options = {
\ 'mode': 'raw',
@@ -520,6 +553,7 @@ 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)
let l:channel_id = ale#socket#Open(a:address, {
\ 'callback': {_, mess -> ale#lsp#HandleMessage(a:conn_id, mess)},
@@ -606,6 +640,15 @@ function! ale#lsp#Send(conn_id, message) abort
throw 'LSP server not initialized yet!'
endif
if g:ale_use_neovim_lsp_api
return luaeval('require("ale.lsp").send_message(_A)', {
\ 'client_id': l:conn.client_id,
\ 'is_notification': a:message[0] == 1 ? v:true : v:false,
\ 'method': a:message[1],
\ 'params': get(a:message, 2, v:null)
\})
endif
let [l:id, l:data] = ale#lsp#CreateMessageData(a:message)
call s:SendMessageData(l:conn, l:data)
@@ -621,11 +664,17 @@ function! ale#lsp#OpenDocument(conn_id, buffer, language_id) abort
if !empty(l:conn) && !has_key(l:conn.open_documents, a:buffer)
if l:conn.is_tsserver
let l:message = ale#lsp#tsserver_message#Open(a:buffer)
call ale#lsp#Send(a:conn_id, l:message)
elseif g:ale_use_neovim_lsp_api
call luaeval('require("ale.lsp").buf_attach(_A)', {
\ 'bufnr': a:buffer,
\ 'client_id': l:conn.client_id,
\})
else
let l:message = ale#lsp#message#DidOpen(a:buffer, a:language_id)
call ale#lsp#Send(a:conn_id, l:message)
endif
call ale#lsp#Send(a:conn_id, l:message)
let l:conn.open_documents[a:buffer] = getbufvar(a:buffer, 'changedtick')
let l:opened = 1
endif
@@ -649,11 +698,17 @@ function! ale#lsp#CloseDocument(buffer) abort
if l:conn.initialized && has_key(l:conn.open_documents, a:buffer)
if l:conn.is_tsserver
let l:message = ale#lsp#tsserver_message#Close(a:buffer)
call ale#lsp#Send(l:conn_id, l:message)
elseif g:ale_use_neovim_lsp_api
call luaeval('require("ale.lsp").buf_detach(_A)', {
\ 'bufnr': a:buffer,
\ 'client_id': l:conn.client_id,
\})
else
let l:message = ale#lsp#message#DidClose(a:buffer)
call ale#lsp#Send(l:conn_id, l:message)
endif
call ale#lsp#Send(l:conn_id, l:message)
call remove(l:conn.open_documents, a:buffer)
let l:closed = 1
endif

View File

@@ -488,6 +488,12 @@ function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort
endfunction
function! s:CheckWithLSP(linter, details) abort
if g:ale_use_neovim_lsp_api && a:linter.lsp isnot# 'tsserver'
" If running an LSP client via Neovim's API then Neovim will
" internally track buffers for changes for us, and we can stop here.
return
endif
let l:buffer = a:details.buffer
let l:info = get(g:ale_buffer_info, l:buffer)
@@ -546,6 +552,7 @@ function! s:OnReadyForCustomRequests(args, linter, lsp_details) abort
let l:id = a:lsp_details.connection_id
let l:request_id = ale#lsp#Send(l:id, a:args.message)
" TODO: Implement this whole flow with the lua API.
if l:request_id > 0 && has_key(a:args, 'handler')
let l:Callback = function('s:HandleLSPResponseToCustomRequests')
call ale#lsp#RegisterCallback(l:id, l:Callback)

62
lua/ale/lsp.lua Normal file
View File

@@ -0,0 +1,62 @@
local module = {}
vim.lsp.set_log_level("debug")
module.start = function(config)
-- Neovim's luaeval sometimes adds a Boolean key to table we need to remove.
if config.init_options[true] ~= nil then
config.init_options[true] = nil
end
config.on_init = function(_, _)
vim.defer_fn(function()
vim.fn["ale#lsp#CallInitCallbacks"](config.name)
end, 0)
end
return vim.lsp.start(config, {
attach = false,
silent = true,
})
end
module.buf_attach = function(args)
return vim.lsp.buf_attach_client(args.bufnr, args.client_id)
end
module.buf_detach = function(args)
return vim.lsp.buf_detach_client(args.bufnr, args.client_id)
end
-- Send a message to an LSP server.
-- Notifications do not need to be handled.
--
-- Returns -1 when a message is sent, but no response is expected
-- 0 when the message is not sent and
-- >= 1 with the message ID when a response is expected.
module.send_message = function(args)
local client = vim.lsp.get_client_by_id(args.client_id)
if args.is_notification then
local success = client.notify(args.method, args.params)
if success then
return -1
end
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)
if success then
return request_id
end
return 0
end
return module

View File

@@ -211,6 +211,16 @@ if g:ale_use_neovim_diagnostics_api && !has('nvim-0.7')
echoerr('Setting g:ale_use_neovim_diagnostics_api to 1 requires Neovim 0.7+.')
endif
let g:ale_use_neovim_lsp_api = get(g:, 'ale_use_neovim_lsp_api', has('nvim-0.8'))
" If 1, replaces ALE's use of jobs and channels to connect to language
" servers, plus the custom code, and instead hooks ALE into Neovim's built-in
" language server tools.
if g:ale_use_neovim_lsp_api && !has('nvim-0.8')
" no-custom-checks
echoerr('Setting g:ale_use_neovim_lsp_api to 1 requires Neovim 0.8+.')
endif
if g:ale_set_balloons is 1 || g:ale_set_balloons is# 'hover'
call ale#balloon#Enable()
endif