#3600 Implement pull model with Neovim Client

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 fe50a711cb
commit e1c8d665d6
3 changed files with 208 additions and 33 deletions

View File

@@ -100,7 +100,7 @@ endfunction
" Handle LSP diagnostics for a given URI.
" The special value 'unchanged' can be used for diagnostics to indicate
" 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)
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: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'))
let l:uri = remove(s:diagnostic_uri_map, a:response.id)
let l:diagnostics = a:response.result.kind is# 'unchanged'
\ ? 'unchanged'
\ : 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'
call ale#lsp_window#HandleShowMessage(
\ 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.
["textDocument/publishDiagnostics"] = function(err, result, _, _)
if err == nil then
vim.fn["ale#lsp_linter#HandleLSPResponse"](config.name, {
jsonrpc = "2.0",
method = "textDocument/publishDiagnostics",
params = result
})
vim.fn["ale#lsp_linter#HandleLSPDiagnostics"](
config.name,
result.uri,
result.diagnostics
)
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, _)
@@ -70,6 +93,16 @@ module.start = function(config)
return vim.fn["ale#lsp#GetLanguage"](config.name, bufnr)
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, {
attach = false,
silent = true,
@@ -93,6 +126,10 @@ end
module.send_message = function(args)
local client = vim.lsp.get_client_by_id(args.client_id)
if client == nil then
return 0
end
if args.is_notification then
-- For notifications we send a request and expect no direct response.
local success = client.notify(args.method, args.params)

View File

@@ -6,6 +6,7 @@ describe("ale.lsp.start", function()
local rpc_connect_calls
local vim_fn_calls
local defer_calls
local nvim_default_capabilities
setup(function()
_G.vim = {
@@ -21,7 +22,7 @@ describe("ale.lsp.start", function()
return "python"
end
if key ~= "ale#lsp_linter#HandleLSPResponse"
if key ~= "ale#lsp_linter#HandleLSPDiagnostics"
and key ~= "ale#lsp#UpdateCapabilities"
and key ~= "ale#lsp#CallInitCallbacks"
then
@@ -49,6 +50,11 @@ describe("ale.lsp.start", function()
return 42
end,
protocol = {
make_client_capabilities = function()
return nvim_default_capabilities
end,
},
},
}
end)
@@ -62,6 +68,9 @@ describe("ale.lsp.start", function()
rpc_connect_calls = {}
vim_fn_calls = {}
defer_calls = {}
nvim_default_capabilities = {
textDocument = {},
}
end)
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)
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()
lsp.start({name = "server:/code"})
@@ -185,39 +212,150 @@ describe("ale.lsp.start", function()
handler_names[key] = true
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, {
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
uri = "file://code/foo.py",
diagnostics = {
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
}
},
})
eq({
{
"ale#lsp_linter#HandleLSPResponse",
"ale#lsp_linter#HandleLSPDiagnostics",
"server:/code",
"file://code/foo.py",
{
jsonrpc = "2.0",
method = "textDocument/publishDiagnostics",
params = {
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
severity = 1,
code = "123",
message = "Warning message",
},
{
lnum = 1,
end_lnum = 2,
col = 3,
end_col = 5,
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)
end)