fix(lsp): support nested workspace registrations #39574

Problem:
Nested workspace capabilities like workspace.fileOperations.didCreate and
workspace.textDocumentContent are not handled consistently for dynamic and
static registration provider lookup.

Solution:
Generate explicit registration-provider mappings from the LSP metadata and use
them when registering and querying capabilities. Add coverage for dynamic and
static nested workspace registrations.
This commit is contained in:
Tristan Knight
2026-05-05 21:36:02 +01:00
committed by GitHub
parent 264fbc0ace
commit ed194b99ac
4 changed files with 348 additions and 50 deletions

View File

@@ -9,6 +9,12 @@ local validate = vim.validate
---@type table<integer,vim.lsp.Client>
local all_clients = {}
---@param provider string
---@param capability string[]?
local function is_nested_server_capability_provider(provider, capability)
return capability ~= nil and #capability > 1 and provider == table.concat(capability, '.')
end
--- @alias vim.lsp.client.on_init_cb fun(client: vim.lsp.Client, init_result: lsp.InitializeResult)
--- @alias vim.lsp.client.on_attach_cb fun(client: vim.lsp.Client, bufnr: integer)
--- @alias vim.lsp.client.on_exit_cb fun(code: integer, signal: integer, client_id: integer)
@@ -644,11 +650,11 @@ function Client:_process_static_registrations()
for method in pairs(lsp.protocol._method_supports_static_registration) do
local capability = lsp.protocol._request_name_to_server_capability[method]
if
vim.tbl_get(self.server_capabilities, capability[1], 'id')
and self:_supports_registration(method)
then
local cap = vim.tbl_get(self.server_capabilities, capability[1])
local provider = self:_registration_provider(method)
local cap = is_nested_server_capability_provider(provider, capability)
and vim.tbl_get(self.server_capabilities, unpack(capability))
or vim.tbl_get(self.server_capabilities, capability[1])
if type(cap) == 'table' and cap.id then
static_registrations[#static_registrations + 1] = {
id = cap.id,
method = method,
@@ -974,8 +980,7 @@ end
--- Get provider for a method to be registered dynamically.
--- @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] or method
return lsp.protocol._request_name_to_registration_provider[method] or method
end
--- @private
@@ -1236,6 +1241,7 @@ function Client:supports_method(method, bufnr)
bufnr = bufnr.bufnr
end
local required_capability = lsp.protocol._request_name_to_server_capability[method]
local has_subcap = required_capability and #required_capability > 1
local is_self_mapping = required_capability
and #required_capability == 1
and required_capability[1] == method
@@ -1249,13 +1255,21 @@ function Client:supports_method(method, bufnr)
end
local provider = self:_registration_provider(method)
local has_subprovider = is_nested_server_capability_provider(provider, required_capability)
local regs = self:_get_registrations(provider, bufnr)
if lsp.protocol._method_supports_dynamic_registration[method] and not regs then
return false
end
if regs then
for _, reg in ipairs(regs or {}) do
if required_capability and #required_capability > 1 then
if has_subprovider then
if
vim.tbl_get(reg, 'registerOptions')
or lsp.protocol._methods_with_no_registration_options[method]
then
return self:_supports_registration(reg.method)
end
elseif has_subcap then
if vim.tbl_get(reg, 'registerOptions', unpack(required_capability, 2)) then
return self:_supports_registration(reg.method)
end
@@ -1303,18 +1317,33 @@ 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
local has_subprovider = is_nested_server_capability_provider(provider, required_capability)
if not dynamic_regs then
-- First check static capabilities
local static_reg = vim.tbl_get(self.server_capabilities, provider)
local static_reg = has_subprovider
and vim.tbl_get(self.server_capabilities, unpack(required_capability))
or vim.tbl_get(self.server_capabilities, provider)
if static_reg then
if not has_subcap or vim.tbl_get(static_reg, unpack(required_capability, 2)) then
if
has_subprovider
or not has_subcap
or vim.tbl_get(static_reg, unpack(required_capability, 2))
then
fn(static_reg)
end
end
else
for _, reg in ipairs(dynamic_regs) do
if not has_subcap or vim.tbl_get(reg, 'registerOptions', unpack(required_capability, 2)) then
fn(vim.tbl_get(reg, 'registerOptions') or {})
local regoptions = vim.tbl_get(reg, 'registerOptions')
if
(
has_subprovider
and (regoptions or lsp.protocol._methods_with_no_registration_options[method])
)
or not has_subcap
or vim.tbl_get(regoptions, unpack(required_capability, 2))
then
fn(regoptions or {})
end
end
end

View File

@@ -1175,7 +1175,6 @@ protocol.Methods = {
-- stylua: ignore start
-- Generated by gen_lsp.lua, keep at end of file.
--- Maps method names to the required client capability
---TODO: also has workspace/* items because spec lacks a top-level "workspaceProvider"
protocol._provider_to_client_registration = {
['callHierarchyProvider'] = { 'textDocument', 'callHierarchy' },
['codeActionProvider'] = { 'textDocument', 'codeAction' },
@@ -1208,19 +1207,121 @@ protocol._provider_to_client_registration = {
['textDocumentSync'] = { 'textDocument', 'synchronization' },
['typeDefinitionProvider'] = { 'textDocument', 'typeDefinition' },
['typeHierarchyProvider'] = { 'textDocument', 'typeHierarchy' },
['workspace.fileOperations.didCreate'] = { 'workspace', 'fileOperations', 'didCreate' },
['workspace.fileOperations.didDelete'] = { 'workspace', 'fileOperations', 'didDelete' },
['workspace.fileOperations.didRename'] = { 'workspace', 'fileOperations', 'didRename' },
['workspace.fileOperations.willCreate'] = { 'workspace', 'fileOperations', 'willCreate' },
['workspace.fileOperations.willDelete'] = { 'workspace', 'fileOperations', 'willDelete' },
['workspace.fileOperations.willRename'] = { 'workspace', 'fileOperations', 'willRename' },
['workspace.textDocumentContent'] = { 'workspace', 'textDocumentContent' },
['workspace/didChangeConfiguration'] = { 'workspace', 'didChangeConfiguration' },
['workspace/didChangeWatchedFiles'] = { 'workspace', 'didChangeWatchedFiles' },
['workspace/didCreateFiles'] = { 'workspace', 'fileOperations', 'didCreate' },
['workspace/didDeleteFiles'] = { 'workspace', 'fileOperations', 'didDelete' },
['workspace/didRenameFiles'] = { 'workspace', 'fileOperations', 'didRename' },
['workspace/textDocumentContent'] = { 'workspace', 'textDocumentContent' },
['workspace/willCreateFiles'] = { 'workspace', 'fileOperations', 'willCreate' },
['workspace/willDeleteFiles'] = { 'workspace', 'fileOperations', 'willDelete' },
['workspace/willRenameFiles'] = { 'workspace', 'fileOperations', 'willRename' },
['workspaceSymbolProvider'] = { 'workspace', 'symbol' },
}
-- stylua: ignore end
-- stylua: ignore start
-- Generated by gen_lsp.lua, keep at end of file.
--- Maps method names to dynamic/static registration providers
protocol._request_name_to_registration_provider = {
['callHierarchy/incomingCalls'] = 'callHierarchyProvider',
['callHierarchy/outgoingCalls'] = 'callHierarchyProvider',
['client/registerCapability'] = 'client/registerCapability',
['client/unregisterCapability'] = 'client/unregisterCapability',
['codeAction/resolve'] = 'codeActionProvider',
['codeLens/resolve'] = 'codeLensProvider',
['completionItem/resolve'] = 'completionProvider',
['documentLink/resolve'] = 'documentLinkProvider',
['$/cancelRequest'] = '$/cancelRequest',
['$/logTrace'] = '$/logTrace',
['$/progress'] = '$/progress',
['$/setTrace'] = '$/setTrace',
['exit'] = 'exit',
['initialize'] = 'initialize',
['initialized'] = 'initialized',
['inlayHint/resolve'] = 'inlayHintProvider',
['notebookDocument/didChange'] = 'notebookDocument/didChange',
['notebookDocument/didClose'] = 'notebookDocument/didClose',
['notebookDocument/didOpen'] = 'notebookDocument/didOpen',
['notebookDocument/didSave'] = 'notebookDocument/didSave',
['shutdown'] = 'shutdown',
['telemetry/event'] = 'telemetry/event',
['textDocument/codeAction'] = 'codeActionProvider',
['textDocument/codeLens'] = 'codeLensProvider',
['textDocument/colorPresentation'] = 'colorProvider',
['textDocument/completion'] = 'completionProvider',
['textDocument/declaration'] = 'declarationProvider',
['textDocument/definition'] = 'definitionProvider',
['textDocument/diagnostic'] = 'diagnosticProvider',
['textDocument/didChange'] = 'textDocumentSync',
['textDocument/didClose'] = 'textDocumentSync',
['textDocument/didOpen'] = 'textDocumentSync',
['textDocument/didSave'] = 'textDocumentSync',
['textDocument/documentColor'] = 'colorProvider',
['textDocument/documentHighlight'] = 'documentHighlightProvider',
['textDocument/documentLink'] = 'documentLinkProvider',
['textDocument/documentSymbol'] = 'documentSymbolProvider',
['textDocument/foldingRange'] = 'foldingRangeProvider',
['textDocument/formatting'] = 'documentFormattingProvider',
['textDocument/hover'] = 'hoverProvider',
['textDocument/implementation'] = 'implementationProvider',
['textDocument/inlayHint'] = 'inlayHintProvider',
['textDocument/inlineCompletion'] = 'inlineCompletionProvider',
['textDocument/inlineValue'] = 'inlineValueProvider',
['textDocument/linkedEditingRange'] = 'linkedEditingRangeProvider',
['textDocument/moniker'] = 'monikerProvider',
['textDocument/onTypeFormatting'] = 'documentOnTypeFormattingProvider',
['textDocument/prepareCallHierarchy'] = 'callHierarchyProvider',
['textDocument/prepareRename'] = 'renameProvider',
['textDocument/prepareTypeHierarchy'] = 'typeHierarchyProvider',
['textDocument/publishDiagnostics'] = 'textDocument/publishDiagnostics',
['textDocument/rangeFormatting'] = 'documentRangeFormattingProvider',
['textDocument/rangesFormatting'] = 'documentRangeFormattingProvider',
['textDocument/references'] = 'referencesProvider',
['textDocument/rename'] = 'renameProvider',
['textDocument/selectionRange'] = 'selectionRangeProvider',
['textDocument/semanticTokens/full'] = 'semanticTokensProvider',
['textDocument/semanticTokens/full/delta'] = 'semanticTokensProvider',
['textDocument/semanticTokens/range'] = 'semanticTokensProvider',
['textDocument/signatureHelp'] = 'signatureHelpProvider',
['textDocument/typeDefinition'] = 'typeDefinitionProvider',
['textDocument/willSave'] = 'textDocumentSync',
['textDocument/willSaveWaitUntil'] = 'textDocumentSync',
['typeHierarchy/subtypes'] = 'typeHierarchy/subtypes',
['typeHierarchy/supertypes'] = 'typeHierarchy/supertypes',
['window/logMessage'] = 'window/logMessage',
['window/showDocument'] = 'window/showDocument',
['window/showMessage'] = 'window/showMessage',
['window/showMessageRequest'] = 'window/showMessageRequest',
['window/workDoneProgress/cancel'] = 'window/workDoneProgress/cancel',
['window/workDoneProgress/create'] = 'window/workDoneProgress/create',
['workspaceSymbol/resolve'] = 'workspaceSymbolProvider',
['workspace/applyEdit'] = 'workspace/applyEdit',
['workspace/codeLens/refresh'] = 'workspace/codeLens/refresh',
['workspace/configuration'] = 'workspace/configuration',
['workspace/diagnostic'] = 'diagnosticProvider',
['workspace/diagnostic/refresh'] = 'workspace/diagnostic/refresh',
['workspace/didChangeConfiguration'] = 'workspace/didChangeConfiguration',
['workspace/didChangeWatchedFiles'] = 'workspace/didChangeWatchedFiles',
['workspace/didChangeWorkspaceFolders'] = 'workspace.workspaceFolders.changeNotifications',
['workspace/didCreateFiles'] = 'workspace.fileOperations.didCreate',
['workspace/didDeleteFiles'] = 'workspace.fileOperations.didDelete',
['workspace/didRenameFiles'] = 'workspace.fileOperations.didRename',
['workspace/executeCommand'] = 'executeCommandProvider',
['workspace/foldingRange/refresh'] = 'workspace/foldingRange/refresh',
['workspace/inlayHint/refresh'] = 'workspace/inlayHint/refresh',
['workspace/inlineValue/refresh'] = 'workspace/inlineValue/refresh',
['workspace/semanticTokens/refresh'] = 'workspace/semanticTokens/refresh',
['workspace/symbol'] = 'workspaceSymbolProvider',
['workspace/textDocumentContent'] = 'workspace.textDocumentContent',
['workspace/textDocumentContent/refresh'] = 'workspace/textDocumentContent/refresh',
['workspace/willCreateFiles'] = 'workspace.fileOperations.willCreate',
['workspace/willDeleteFiles'] = 'workspace.fileOperations.willDelete',
['workspace/willRenameFiles'] = 'workspace.fileOperations.willRename',
['workspace/workspaceFolders'] = 'workspace.workspaceFolders',
}
-- stylua: ignore end
-- stylua: ignore start
-- Generated by gen_lsp.lua, keep at end of file.
--- Maps method names to the required server capability
@@ -1389,39 +1490,25 @@ protocol._method_supports_dynamic_registration = {
-- stylua: ignore start
-- Generated by gen_lsp.lua, keep at end of file.
protocol._method_supports_static_registration = {
['textDocument/codeAction'] = true,
['textDocument/codeLens'] = true,
['callHierarchy/incomingCalls'] = true,
['callHierarchy/outgoingCalls'] = true,
['textDocument/colorPresentation'] = true,
['textDocument/completion'] = true,
['textDocument/declaration'] = true,
['textDocument/definition'] = true,
['textDocument/diagnostic'] = true,
['textDocument/didChange'] = true,
['textDocument/documentColor'] = true,
['textDocument/documentHighlight'] = true,
['textDocument/documentLink'] = true,
['textDocument/documentSymbol'] = true,
['textDocument/foldingRange'] = true,
['textDocument/formatting'] = true,
['textDocument/hover'] = true,
['textDocument/implementation'] = true,
['textDocument/inlayHint'] = true,
['textDocument/inlineCompletion'] = true,
['textDocument/inlineValue'] = true,
['textDocument/linkedEditingRange'] = true,
['textDocument/moniker'] = true,
['textDocument/onTypeFormatting'] = true,
['textDocument/prepareCallHierarchy'] = true,
['textDocument/prepareTypeHierarchy'] = true,
['textDocument/rangeFormatting'] = true,
['textDocument/references'] = true,
['textDocument/rename'] = true,
['textDocument/selectionRange'] = true,
['textDocument/semanticTokens/full'] = true,
['textDocument/signatureHelp'] = true,
['textDocument/semanticTokens/full/delta'] = true,
['textDocument/typeDefinition'] = true,
['workspace/executeCommand'] = true,
['workspace/symbol'] = true,
['workspace/textDocumentContent'] = true,
}
-- stylua: ignore end