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 344d984ed2)
This commit is contained in:
Tristan Knight
2026-05-01 17:04:18 +01:00
committed by zeertzjq
parent e230ff0439
commit 8919b02eba
2 changed files with 51 additions and 5 deletions

View File

@@ -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

View File

@@ -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()