From 344d984ed2b82bbe0373eb9322368373ba66f2b5 Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Fri, 1 May 2026 17:04:18 +0100 Subject: [PATCH] fix(lsp): dynamic registration for off-spec method #39544 Problem: LSP clients previously did not handle dynamic registration for off-spec methods Solution: Update the client logic to assume support for dynamic registration when the method is unknown. Adjust the registration provider fallback and enhance tests to verify correct behaviour for unknown methods and their registration options. This improves compatibility with servers using custom dynamic registrations. AI-assisted: OpenCode --- runtime/lua/vim/lsp/client.lua | 14 +++++++--- test/functional/plugin/lsp_spec.lua | 42 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 7c0d36d5ff..92c9127ea7 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -963,6 +963,10 @@ function Client:_supports_registration(method) end local provider = self:_registration_provider(method) local capability_path = lsp.protocol._provider_to_client_registration[provider] + if not capability_path then + -- If we don't know about the method, assume the client supports dynamic registration for it. + return true + end local capability = vim.tbl_get(self.capabilities, unpack(capability_path)) return type(capability) == 'table' and capability.dynamicRegistration end @@ -971,7 +975,7 @@ end --- @param method vim.lsp.protocol.Method | vim.lsp.protocol.Method.Registration function Client:_registration_provider(method) local capability_path = lsp.protocol._request_name_to_server_capability[method] - return capability_path and capability_path[1] + return capability_path and capability_path[1] or method end --- @private @@ -1265,6 +1269,10 @@ function Client:supports_method(method, bufnr) return false end + if required_capability == nil and next(self.registrations[method] or {}) ~= nil then + return false + end + -- If we don't know about the method, or if it is a self-mapping(method=required_capability) -- assume that the client supports it. -- This needs to be at the end, so that dynamic_capabilities are checked first. @@ -1295,9 +1303,7 @@ function Client:_provider_foreach(method, fn) local required_capability = lsp.protocol._request_name_to_server_capability[method] local dynamic_regs = self:_get_registrations(provider) local has_subcap = required_capability and #required_capability > 1 - if not provider then - return - elseif not dynamic_regs then + if not dynamic_regs then -- First check static capabilities local static_reg = vim.tbl_get(self.server_capabilities, provider) if static_reg then diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 0e375d62da..58939bf3c0 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -3381,10 +3381,42 @@ describe('LSP', function() }, { client_id = client_id }) check('workspace/didChangeConfiguration', nil, 'section') + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'unknown-method-id', + method = 'unknown-method', + registerOptions = { + some_opt = 'unknown-dummy-opt', + }, + }, + }, + }, { client_id = client_id }) + check('unknown-method', nil, 'some_opt') + + check('unknown-method-2') + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'unknown-method-2-id', + method = 'unknown-method-2', + registerOptions = { + documentSelector = { + { + pattern = root_dir .. '/*.foo', + }, + }, + }, + }, + }, + }, { client_id = client_id }) + check('unknown-method-2') + check('unknown-method-2', tmpfile) + return result end) - eq(21, #result) + eq(25, #result) eq({ method = 'textDocument/formatting', supported = false }, result[1]) eq({ method = 'textDocument/formatting', supported = true, fname = tmpfile }, result[2]) eq({ method = 'textDocument/rangeFormatting', supported = true }, result[3]) @@ -3419,6 +3451,14 @@ describe('LSP', function() { method = 'workspace/didChangeConfiguration', supported = true, cap = { 'dummy-section' } }, result[21] ) + eq({ + method = 'unknown-method', + supported = true, + cap = { 'unknown-dummy-opt' }, + }, result[22]) + eq({ method = 'unknown-method-2', supported = true }, result[23]) + eq({ method = 'unknown-method-2', supported = false }, result[24]) + eq({ method = 'unknown-method-2', supported = true, fname = tmpfile }, result[25]) end) it('identifies client dynamic registration capability', function()