diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 92c9127ea7..3df6a03174 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -9,6 +9,12 @@ local validate = vim.validate ---@type table 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 diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 93f67b52b9..46b5602300 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -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 diff --git a/src/gen/gen_lsp.lua b/src/gen/gen_lsp.lua index 02551a6203..f958e89fb8 100755 --- a/src/gen/gen_lsp.lua +++ b/src/gen/gen_lsp.lua @@ -134,12 +134,69 @@ local function compare_method(a, b) return to_luaname(a.method) < to_luaname(b.method) end +--- @param item vim._gen_lsp.Request|vim._gen_lsp.Notification +--- @return string? +local function registration_provider(item) + local server_capability = item.serverCapability + if not server_capability then + return nil + end + if vim.startswith(server_capability, 'workspace.') then + return server_capability + end + return server_capability:match('^[^%.]+') +end + +---@param structures table +---@param type vim._gen_lsp.Type +---@param seen? table +---@return boolean +local function supports_static_registration(structures, type, seen) + if type.kind == 'reference' then + if type.name == 'StaticRegistrationOptions' then + return true + end + + seen = seen or {} + if seen[type.name] then + return false + end + seen[type.name] = true + + local structure = structures[type.name] + if not structure then + return false + end + local bases = {} + vim.list_extend(bases, structure.extends or {}) + vim.list_extend(bases, structure.mixins or {}) + for _, base in ipairs(bases) do + if supports_static_registration(structures, base, seen) then + return true + end + end + elseif type.kind == 'and' or type.kind == 'or' then + for _, item in ipairs(type.items) do + if supports_static_registration(structures, item, seen) then + return true + end + end + end + + return false +end + ---@param protocol vim._gen_lsp.Protocol local function write_to_vim_protocol(protocol) local all = {} --- @type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[] vim.list_extend(all, protocol.notifications) vim.list_extend(all, protocol.requests) + local structures = {} --- @type table + for _, structure in ipairs(protocol.structures) do + structures[structure.name] = structure + end + table.sort(all, compare_method) table.sort(protocol.requests, compare_method) table.sort(protocol.notifications, compare_method) @@ -224,18 +281,20 @@ local function write_to_vim_protocol(protocol) '-- 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 = {', }) local providers = {} --- @type table for _, item in ipairs(all) do - local base_provider = item.serverCapability and item.serverCapability:match('^[^%.]+') - if item.registrationOptions and not providers[base_provider] and item.clientCapability then - if item.clientCapability == item.serverCapability then - base_provider = nil + local provider = registration_provider(item) + if item.registrationOptions and not providers[provider] and item.clientCapability then + if + item.clientCapability == item.serverCapability + and not (item.serverCapability and vim.startswith(item.serverCapability, 'workspace.')) + then + provider = nil end - local key = base_provider or item.method + local key = provider or item.method providers[key] = item.clientCapability end end @@ -258,6 +317,22 @@ local function write_to_vim_protocol(protocol) output[#output + 1] = '}' output[#output + 1] = '-- stylua: ignore end' + vim.list_extend(output, { + '', + '-- 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 = {', + }) + + for _, item in ipairs(all) do + local provider = registration_provider(item) or item.method + output[#output + 1] = (" ['%s'] = '%s',"):format(item.method, provider) + end + + output[#output + 1] = '}' + output[#output + 1] = '-- stylua: ignore end' + vim.list_extend(output, { '', '-- stylua: ignore start', @@ -334,10 +409,25 @@ local function write_to_vim_protocol(protocol) 'protocol._method_supports_static_registration = {', }) + local static_registration_providers = {} --- @type table for _, item in ipairs(all) do if item.registrationOptions - and (item.serverCapability and not item.serverCapability:find('%.')) + and item.serverCapability + and supports_static_registration(structures, item.registrationOptions) + then + static_registration_providers[registration_provider(item)] = true + end + end + + for _, item in ipairs(all) do + local provider = registration_provider(item) + local has_static_registration = item.registrationOptions + and supports_static_registration(structures, item.registrationOptions) + if + item.serverCapability + and static_registration_providers[provider] + and (has_static_registration or item.serverCapability == provider) then output[#output + 1] = (" ['%s'] = %s,"):format(item.method, true) end diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 58939bf3c0..79727add44 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -998,6 +998,8 @@ describe('LSP', function() eq(true, client:supports_method('textDocument/hover')) eq(false, client:supports_method('textDocument/definition')) + eq(false, client:supports_method('workspace/didCreateFiles')) + -- Self-mapped methods do not have a related server capability and should be assumed -- to be supported. eq(true, client:supports_method('shutdown')) @@ -3198,6 +3200,14 @@ describe('LSP', function() didChangeConfiguration = { dynamicRegistration = true, }, + fileOperations = { + didCreate = { + dynamicRegistration = true, + }, + }, + textDocumentContent = { + dynamicRegistration = true, + }, }, }, })) @@ -3381,6 +3391,42 @@ describe('LSP', function() }, { client_id = client_id }) check('workspace/didChangeConfiguration', nil, 'section') + check('workspace/didCreateFiles') + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'didCreateFiles-id', + method = 'workspace/didCreateFiles', + registerOptions = { + label = 'did-create-files', + filters = { + { + scheme = 'file', + pattern = { + glob = '**/*.foo', + }, + }, + }, + }, + }, + }, + }, { client_id = client_id }) + check('workspace/didCreateFiles', nil, 'label') + + check('workspace/textDocumentContent') + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'textDocumentContent-id', + method = 'workspace/textDocumentContent', + registerOptions = { + schemes = { 'git', 'untitled' }, + }, + }, + }, + }, { client_id = client_id }) + check('workspace/textDocumentContent', nil, 'schemes') + vim.lsp.handlers['client/registerCapability'](nil, { registrations = { { @@ -3416,7 +3462,7 @@ describe('LSP', function() return result end) - eq(25, #result) + eq(29, #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]) @@ -3451,14 +3497,25 @@ describe('LSP', function() { method = 'workspace/didChangeConfiguration', supported = true, cap = { 'dummy-section' } }, result[21] ) + eq({ method = 'workspace/didCreateFiles', supported = false }, result[22]) + eq( + { method = 'workspace/didCreateFiles', supported = true, cap = { 'did-create-files' } }, + result[23] + ) + eq({ method = 'workspace/textDocumentContent', supported = false }, result[24]) + eq({ + method = 'workspace/textDocumentContent', + supported = true, + cap = { { 'git', 'untitled' } }, + }, result[25]) 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]) + }, result[26]) + eq({ method = 'unknown-method-2', supported = true }, result[27]) + eq({ method = 'unknown-method-2', supported = false }, result[28]) + eq({ method = 'unknown-method-2', supported = true, fname = tmpfile }, result[29]) end) it('identifies client dynamic registration capability', function() @@ -3523,6 +3580,12 @@ describe('LSP', function() identifier = 'diag-ident-static', workspaceDiagnostics = true, }, + workspace = { + textDocumentContent = { + id = 'text-document-content-registration', + schemes = { 'git', 'untitled' }, + }, + }, }, }) @@ -3585,6 +3648,35 @@ describe('LSP', function() return result end) ) + + eq( + { + { + id = 'text-document-content-registration', + method = 'workspace/textDocumentContent', + registerOptions = { + id = 'text-document-content-registration', + schemes = { 'git', 'untitled' }, + }, + }, + }, + sort_method(exec_lua(function() + local client = assert(vim.lsp.get_client_by_id(client_id)) + return client.dynamic_capabilities:get('workspace.textDocumentContent') + end)) + ) + + eq( + { { 'git', 'untitled' } }, + exec_lua(function() + local client = assert(vim.lsp.get_client_by_id(client_id)) + local result = {} + client:_provider_foreach('workspace/textDocumentContent', function(cap) + table.insert(result, cap.schemes) + end) + return result + end) + ) end) end)