#3600 Implement pull model with Neovim Client
Some checks failed
CI / build_image (push) Has been cancelled
CI / test_ale (--linters-only) (push) Has been cancelled
CI / test_ale (--lua-only) (push) Has been cancelled
CI / test_ale (--neovim-07-only) (push) Has been cancelled
CI / test_ale (--neovim-08-only) (push) Has been cancelled
CI / test_ale (--vim-80-only) (push) Has been cancelled
CI / test_ale (--vim-90-only) (push) Has been cancelled

Implement the diagnostics pull model with the LSP Neovim client. We
must handle messages a little different and tweak client capabilities
for pull diagnostics to work through the Neovim client.
This commit is contained in:
w0rp
2025-03-23 16:08:18 +00:00
parent f90e72ae1f
commit dd23b92ee9
3 changed files with 208 additions and 33 deletions

View File

@@ -100,7 +100,7 @@ endfunction
" Handle LSP diagnostics for a given URI. " Handle LSP diagnostics for a given URI.
" The special value 'unchanged' can be used for diagnostics to indicate " The special value 'unchanged' can be used for diagnostics to indicate
" that diagnostics haven't changed since we last checked. " that diagnostics haven't changed since we last checked.
function! s:HandleLSPDiagnostics(conn_id, uri, diagnostics) abort function! ale#lsp_linter#HandleLSPDiagnostics(conn_id, uri, diagnostics) abort
let l:linter = get(s:lsp_linter_map, a:conn_id) let l:linter = get(s:lsp_linter_map, a:conn_id)
if empty(l:linter) if empty(l:linter)
@@ -233,14 +233,14 @@ function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort
let l:uri = a:response.params.uri let l:uri = a:response.params.uri
let l:diagnostics = a:response.params.diagnostics let l:diagnostics = a:response.params.diagnostics
call s:HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics) call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
elseif has_key(s:diagnostic_uri_map, get(a:response, 'id')) elseif has_key(s:diagnostic_uri_map, get(a:response, 'id'))
let l:uri = remove(s:diagnostic_uri_map, a:response.id) let l:uri = remove(s:diagnostic_uri_map, a:response.id)
let l:diagnostics = a:response.result.kind is# 'unchanged' let l:diagnostics = a:response.result.kind is# 'unchanged'
\ ? 'unchanged' \ ? 'unchanged'
\ : a:response.result.items \ : a:response.result.items
call s:HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics) call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
elseif l:method is# 'window/showMessage' elseif l:method is# 'window/showMessage'
call ale#lsp_window#HandleShowMessage( call ale#lsp_window#HandleShowMessage(
\ s:lsp_linter_map[a:conn_id].name, \ s:lsp_linter_map[a:conn_id].name,

View File

@@ -38,13 +38,36 @@ module.start = function(config)
-- functions so all of the functionality in ALE works. -- functions so all of the functionality in ALE works.
["textDocument/publishDiagnostics"] = function(err, result, _, _) ["textDocument/publishDiagnostics"] = function(err, result, _, _)
if err == nil then if err == nil then
vim.fn["ale#lsp_linter#HandleLSPResponse"](config.name, { vim.fn["ale#lsp_linter#HandleLSPDiagnostics"](
jsonrpc = "2.0", config.name,
method = "textDocument/publishDiagnostics", result.uri,
params = result result.diagnostics
}) )
end end
end end,
-- Handle pull model diagnostic data.
["textDocument/diagnostic"] = function(err, result, request, _)
if err == nil then
local diagnostics
if result.kind == "unchanged" then
diagnostics = "unchanged"
else
diagnostics = result.items
end
vim.fn["ale#lsp_linter#HandleLSPDiagnostics"](
config.name,
request.params.textDocument.uri,
diagnostics
)
end
end,
-- When the pull model is enabled we have to handle and return
-- some kind of data for a server diagnostic refresh request.
["workspace/diagnostic/refresh"] = function()
return {}
end,
} }
config.on_init = function(client, _) config.on_init = function(client, _)
@@ -70,6 +93,16 @@ module.start = function(config)
return vim.fn["ale#lsp#GetLanguage"](config.name, bufnr) return vim.fn["ale#lsp#GetLanguage"](config.name, bufnr)
end end
local capabilities = vim.lsp.protocol.make_client_capabilities()
-- Language servers like Pyright do not enable the diagnostics pull model
-- unless dynamicRegistration is enabled for diagnostics.
if capabilities.textDocument.diagnostic ~= nil then
capabilities.textDocument.diagnostic.dynamicRegistration = true
config.capabilities = capabilities
end
---@diagnostic disable-next-line: missing-fields
return vim.lsp.start(config, { return vim.lsp.start(config, {
attach = false, attach = false,
silent = true, silent = true,
@@ -93,6 +126,10 @@ end
module.send_message = function(args) module.send_message = function(args)
local client = vim.lsp.get_client_by_id(args.client_id) local client = vim.lsp.get_client_by_id(args.client_id)
if client == nil then
return 0
end
if args.is_notification then if args.is_notification then
-- For notifications we send a request and expect no direct response. -- For notifications we send a request and expect no direct response.
local success = client.notify(args.method, args.params) local success = client.notify(args.method, args.params)

View File

@@ -6,6 +6,7 @@ describe("ale.lsp.start", function()
local rpc_connect_calls local rpc_connect_calls
local vim_fn_calls local vim_fn_calls
local defer_calls local defer_calls
local nvim_default_capabilities
setup(function() setup(function()
_G.vim = { _G.vim = {
@@ -21,7 +22,7 @@ describe("ale.lsp.start", function()
return "python" return "python"
end end
if key ~= "ale#lsp_linter#HandleLSPResponse" if key ~= "ale#lsp_linter#HandleLSPDiagnostics"
and key ~= "ale#lsp#UpdateCapabilities" and key ~= "ale#lsp#UpdateCapabilities"
and key ~= "ale#lsp#CallInitCallbacks" and key ~= "ale#lsp#CallInitCallbacks"
then then
@@ -49,6 +50,11 @@ describe("ale.lsp.start", function()
return 42 return 42
end, end,
protocol = {
make_client_capabilities = function()
return nvim_default_capabilities
end,
},
}, },
} }
end) end)
@@ -62,6 +68,9 @@ describe("ale.lsp.start", function()
rpc_connect_calls = {} rpc_connect_calls = {}
vim_fn_calls = {} vim_fn_calls = {}
defer_calls = {} defer_calls = {}
nvim_default_capabilities = {
textDocument = {},
}
end) end)
it("should start lsp programs with the correct arguments", function() it("should start lsp programs with the correct arguments", function()
@@ -148,6 +157,24 @@ describe("ale.lsp.start", function()
eq({{"ale#lsp#GetLanguage", "server:/code", 347}}, vim_fn_calls) eq({{"ale#lsp#GetLanguage", "server:/code", 347}}, vim_fn_calls)
end) end)
it("should enable dynamicRegistration for the pull model", function()
nvim_default_capabilities = {textDocument = {diagnostic = {}}}
lsp.start({name = "server:/code"})
eq(1, #start_calls)
eq(
{
textDocument = {
diagnostic = {
dynamicRegistration = true,
},
},
},
start_calls[1][1].capabilities
)
end)
it("should initialize clients with ALE correctly", function() it("should initialize clients with ALE correctly", function()
lsp.start({name = "server:/code"}) lsp.start({name = "server:/code"})
@@ -185,39 +212,150 @@ describe("ale.lsp.start", function()
handler_names[key] = true handler_names[key] = true
end end
eq({["textDocument/publishDiagnostics"] = true}, handler_names) eq({
["textDocument/publishDiagnostics"] = true,
["textDocument/diagnostic"] = true,
["workspace/diagnostic/refresh"] = true,
}, handler_names)
end)
it("should handle push model published diagnostics", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
local handlers = start_calls[1][1].handlers
eq("function", type(handlers["textDocument/publishDiagnostics"]))
handlers["textDocument/publishDiagnostics"](nil, { handlers["textDocument/publishDiagnostics"](nil, {
{ uri = "file://code/foo.py",
lnum = 1, diagnostics = {
end_lnum = 2, {
col = 3, lnum = 1,
end_col = 5, end_lnum = 2,
severity = 1, col = 3,
code = "123", end_col = 5,
message = "Warning message", severity = 1,
code = "123",
message = "Warning message",
}
}, },
}) })
eq({ eq({
{ {
"ale#lsp_linter#HandleLSPResponse", "ale#lsp_linter#HandleLSPDiagnostics",
"server:/code", "server:/code",
"file://code/foo.py",
{ {
jsonrpc = "2.0", {
method = "textDocument/publishDiagnostics", lnum = 1,
params = { end_lnum = 2,
{ col = 3,
lnum = 1, end_col = 5,
end_lnum = 2, severity = 1,
col = 3, code = "123",
end_col = 5, message = "Warning message",
severity = 1, },
code = "123", },
message = "Warning message", },
}, }, vim_fn_calls)
end)
it("should respond to workspace diagnostic refresh requests", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
local handlers = start_calls[1][1].handlers
eq("function", type(handlers["workspace/diagnostic/refresh"]))
eq({}, handlers["workspace/diagnostic/refresh"]())
end)
it("should handle pull model diagnostics", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
local handlers = start_calls[1][1].handlers
eq("function", type(handlers["textDocument/diagnostic"]))
handlers["textDocument/diagnostic"](
nil,
{
kind = "full",
items = {
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
} }
} },
},
{
params = {
textDocument = {
uri = "file://code/foo.py",
},
},
}
)
eq({
{
"ale#lsp_linter#HandleLSPDiagnostics",
"server:/code",
"file://code/foo.py",
{
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
},
},
},
}, vim_fn_calls)
end)
it("should handle unchanged pull model diagnostics", function()
lsp.start({name = "server:/code"})
eq(1, #start_calls)
local handlers = start_calls[1][1].handlers
eq("function", type(handlers["textDocument/diagnostic"]))
handlers["textDocument/diagnostic"](
nil,
{kind = "unchanged"},
{
params = {
textDocument = {
uri = "file://code/foo.py",
},
},
}
)
eq({
{
"ale#lsp_linter#HandleLSPDiagnostics",
"server:/code",
"file://code/foo.py",
"unchanged",
}, },
}, vim_fn_calls) }, vim_fn_calls)
end) end)