From e1c8d665d69181f283d45411287bb25bb1b6da44 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sun, 23 Mar 2025 16:08:18 +0000 Subject: [PATCH] #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. --- autoload/ale/lsp_linter.vim | 6 +- lua/ale/lsp.lua | 49 ++++++++-- test/lua/ale_lsp_spec.lua | 186 +++++++++++++++++++++++++++++++----- 3 files changed, 208 insertions(+), 33 deletions(-) diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index 9936263d..3b3c403c 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -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, diff --git a/lua/ale/lsp.lua b/lua/ale/lsp.lua index b5529af4..2d0f6e92 100644 --- a/lua/ale/lsp.lua +++ b/lua/ale/lsp.lua @@ -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) diff --git a/test/lua/ale_lsp_spec.lua b/test/lua/ale_lsp_spec.lua index 8f9b2974..b37fbe28 100644 --- a/test/lua/ale_lsp_spec.lua +++ b/test/lua/ale_lsp_spec.lua @@ -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)