Skip to content

Commit dd23b92

Browse files
committed
#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.
1 parent f90e72a commit dd23b92

File tree

3 files changed

+208
-33
lines changed

3 files changed

+208
-33
lines changed

autoload/ale/lsp_linter.vim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ endfunction
100100
" Handle LSP diagnostics for a given URI.
101101
" The special value 'unchanged' can be used for diagnostics to indicate
102102
" that diagnostics haven't changed since we last checked.
103-
function! s:HandleLSPDiagnostics(conn_id, uri, diagnostics) abort
103+
function! ale#lsp_linter#HandleLSPDiagnostics(conn_id, uri, diagnostics) abort
104104
let l:linter = get(s:lsp_linter_map, a:conn_id)
105105

106106
if empty(l:linter)
@@ -233,14 +233,14 @@ function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort
233233
let l:uri = a:response.params.uri
234234
let l:diagnostics = a:response.params.diagnostics
235235

236-
call s:HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
236+
call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
237237
elseif has_key(s:diagnostic_uri_map, get(a:response, 'id'))
238238
let l:uri = remove(s:diagnostic_uri_map, a:response.id)
239239
let l:diagnostics = a:response.result.kind is# 'unchanged'
240240
\ ? 'unchanged'
241241
\ : a:response.result.items
242242

243-
call s:HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
243+
call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
244244
elseif l:method is# 'window/showMessage'
245245
call ale#lsp_window#HandleShowMessage(
246246
\ s:lsp_linter_map[a:conn_id].name,

lua/ale/lsp.lua

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,36 @@ module.start = function(config)
3838
-- functions so all of the functionality in ALE works.
3939
["textDocument/publishDiagnostics"] = function(err, result, _, _)
4040
if err == nil then
41-
vim.fn["ale#lsp_linter#HandleLSPResponse"](config.name, {
42-
jsonrpc = "2.0",
43-
method = "textDocument/publishDiagnostics",
44-
params = result
45-
})
41+
vim.fn["ale#lsp_linter#HandleLSPDiagnostics"](
42+
config.name,
43+
result.uri,
44+
result.diagnostics
45+
)
4646
end
47-
end
47+
end,
48+
-- Handle pull model diagnostic data.
49+
["textDocument/diagnostic"] = function(err, result, request, _)
50+
if err == nil then
51+
local diagnostics
52+
53+
if result.kind == "unchanged" then
54+
diagnostics = "unchanged"
55+
else
56+
diagnostics = result.items
57+
end
58+
59+
vim.fn["ale#lsp_linter#HandleLSPDiagnostics"](
60+
config.name,
61+
request.params.textDocument.uri,
62+
diagnostics
63+
)
64+
end
65+
end,
66+
-- When the pull model is enabled we have to handle and return
67+
-- some kind of data for a server diagnostic refresh request.
68+
["workspace/diagnostic/refresh"] = function()
69+
return {}
70+
end,
4871
}
4972

5073
config.on_init = function(client, _)
@@ -70,6 +93,16 @@ module.start = function(config)
7093
return vim.fn["ale#lsp#GetLanguage"](config.name, bufnr)
7194
end
7295

96+
local capabilities = vim.lsp.protocol.make_client_capabilities()
97+
98+
-- Language servers like Pyright do not enable the diagnostics pull model
99+
-- unless dynamicRegistration is enabled for diagnostics.
100+
if capabilities.textDocument.diagnostic ~= nil then
101+
capabilities.textDocument.diagnostic.dynamicRegistration = true
102+
config.capabilities = capabilities
103+
end
104+
105+
---@diagnostic disable-next-line: missing-fields
73106
return vim.lsp.start(config, {
74107
attach = false,
75108
silent = true,
@@ -93,6 +126,10 @@ end
93126
module.send_message = function(args)
94127
local client = vim.lsp.get_client_by_id(args.client_id)
95128

129+
if client == nil then
130+
return 0
131+
end
132+
96133
if args.is_notification then
97134
-- For notifications we send a request and expect no direct response.
98135
local success = client.notify(args.method, args.params)

test/lua/ale_lsp_spec.lua

Lines changed: 162 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ describe("ale.lsp.start", function()
66
local rpc_connect_calls
77
local vim_fn_calls
88
local defer_calls
9+
local nvim_default_capabilities
910

1011
setup(function()
1112
_G.vim = {
@@ -21,7 +22,7 @@ describe("ale.lsp.start", function()
2122
return "python"
2223
end
2324

24-
if key ~= "ale#lsp_linter#HandleLSPResponse"
25+
if key ~= "ale#lsp_linter#HandleLSPDiagnostics"
2526
and key ~= "ale#lsp#UpdateCapabilities"
2627
and key ~= "ale#lsp#CallInitCallbacks"
2728
then
@@ -49,6 +50,11 @@ describe("ale.lsp.start", function()
4950

5051
return 42
5152
end,
53+
protocol = {
54+
make_client_capabilities = function()
55+
return nvim_default_capabilities
56+
end,
57+
},
5258
},
5359
}
5460
end)
@@ -62,6 +68,9 @@ describe("ale.lsp.start", function()
6268
rpc_connect_calls = {}
6369
vim_fn_calls = {}
6470
defer_calls = {}
71+
nvim_default_capabilities = {
72+
textDocument = {},
73+
}
6574
end)
6675

6776
it("should start lsp programs with the correct arguments", function()
@@ -148,6 +157,24 @@ describe("ale.lsp.start", function()
148157
eq({{"ale#lsp#GetLanguage", "server:/code", 347}}, vim_fn_calls)
149158
end)
150159

160+
it("should enable dynamicRegistration for the pull model", function()
161+
nvim_default_capabilities = {textDocument = {diagnostic = {}}}
162+
163+
lsp.start({name = "server:/code"})
164+
eq(1, #start_calls)
165+
166+
eq(
167+
{
168+
textDocument = {
169+
diagnostic = {
170+
dynamicRegistration = true,
171+
},
172+
},
173+
},
174+
start_calls[1][1].capabilities
175+
)
176+
end)
177+
151178
it("should initialize clients with ALE correctly", function()
152179
lsp.start({name = "server:/code"})
153180

@@ -185,39 +212,150 @@ describe("ale.lsp.start", function()
185212
handler_names[key] = true
186213
end
187214

188-
eq({["textDocument/publishDiagnostics"] = true}, handler_names)
215+
eq({
216+
["textDocument/publishDiagnostics"] = true,
217+
["textDocument/diagnostic"] = true,
218+
["workspace/diagnostic/refresh"] = true,
219+
}, handler_names)
220+
end)
221+
222+
it("should handle push model published diagnostics", function()
223+
lsp.start({name = "server:/code"})
224+
225+
eq(1, #start_calls)
226+
227+
local handlers = start_calls[1][1].handlers
228+
229+
eq("function", type(handlers["textDocument/publishDiagnostics"]))
189230

190231
handlers["textDocument/publishDiagnostics"](nil, {
191-
{
192-
lnum = 1,
193-
end_lnum = 2,
194-
col = 3,
195-
end_col = 5,
196-
severity = 1,
197-
code = "123",
198-
message = "Warning message",
232+
uri = "file://code/foo.py",
233+
diagnostics = {
234+
{
235+
lnum = 1,
236+
end_lnum = 2,
237+
col = 3,
238+
end_col = 5,
239+
severity = 1,
240+
code = "123",
241+
message = "Warning message",
242+
}
199243
},
200244
})
201245

202246
eq({
203247
{
204-
"ale#lsp_linter#HandleLSPResponse",
248+
"ale#lsp_linter#HandleLSPDiagnostics",
205249
"server:/code",
250+
"file://code/foo.py",
206251
{
207-
jsonrpc = "2.0",
208-
method = "textDocument/publishDiagnostics",
209-
params = {
210-
{
211-
lnum = 1,
212-
end_lnum = 2,
213-
col = 3,
214-
end_col = 5,
215-
severity = 1,
216-
code = "123",
217-
message = "Warning message",
218-
},
252+
{
253+
lnum = 1,
254+
end_lnum = 2,
255+
col = 3,
256+
end_col = 5,
257+
severity = 1,
258+
code = "123",
259+
message = "Warning message",
260+
},
261+
},
262+
},
263+
}, vim_fn_calls)
264+
end)
265+
266+
it("should respond to workspace diagnostic refresh requests", function()
267+
lsp.start({name = "server:/code"})
268+
269+
eq(1, #start_calls)
270+
271+
local handlers = start_calls[1][1].handlers
272+
273+
eq("function", type(handlers["workspace/diagnostic/refresh"]))
274+
275+
eq({}, handlers["workspace/diagnostic/refresh"]())
276+
end)
277+
278+
it("should handle pull model diagnostics", function()
279+
lsp.start({name = "server:/code"})
280+
281+
eq(1, #start_calls)
282+
283+
local handlers = start_calls[1][1].handlers
284+
285+
eq("function", type(handlers["textDocument/diagnostic"]))
286+
287+
handlers["textDocument/diagnostic"](
288+
nil,
289+
{
290+
kind = "full",
291+
items = {
292+
{
293+
lnum = 1,
294+
end_lnum = 2,
295+
col = 3,
296+
end_col = 5,
297+
severity = 1,
298+
code = "123",
299+
message = "Warning message",
219300
}
220-
}
301+
},
302+
},
303+
{
304+
params = {
305+
textDocument = {
306+
uri = "file://code/foo.py",
307+
},
308+
},
309+
}
310+
)
311+
312+
eq({
313+
{
314+
"ale#lsp_linter#HandleLSPDiagnostics",
315+
"server:/code",
316+
"file://code/foo.py",
317+
{
318+
{
319+
lnum = 1,
320+
end_lnum = 2,
321+
col = 3,
322+
end_col = 5,
323+
severity = 1,
324+
code = "123",
325+
message = "Warning message",
326+
},
327+
},
328+
},
329+
}, vim_fn_calls)
330+
end)
331+
332+
it("should handle unchanged pull model diagnostics", function()
333+
lsp.start({name = "server:/code"})
334+
335+
eq(1, #start_calls)
336+
337+
local handlers = start_calls[1][1].handlers
338+
339+
eq("function", type(handlers["textDocument/diagnostic"]))
340+
341+
handlers["textDocument/diagnostic"](
342+
nil,
343+
{kind = "unchanged"},
344+
{
345+
params = {
346+
textDocument = {
347+
uri = "file://code/foo.py",
348+
},
349+
},
350+
}
351+
)
352+
353+
eq({
354+
{
355+
"ale#lsp_linter#HandleLSPDiagnostics",
356+
"server:/code",
357+
"file://code/foo.py",
358+
"unchanged",
221359
},
222360
}, vim_fn_calls)
223361
end)

0 commit comments

Comments
 (0)