From 8919b02eba5056578793f78fd0411f15f09b609a 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 (cherry picked from commit 344d984ed2b82bbe0373eb9322368373ba66f2b5) --- 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 7692759fb8..f413cc812a 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -948,6 +948,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 @@ -956,7 +960,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 @@ -1250,6 +1254,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. @@ -1280,9 +1288,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 b9bbfba7c0..2e68f2cbfe 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -3419,10 +3419,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]) @@ -3457,6 +3489,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()