From b99cdd08de42804b0ea2448d6f9f4e476b931b80 Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Sat, 14 Feb 2026 15:50:48 +0000 Subject: [PATCH] refactor(lsp): centralize provider capability resolution #37221 - Refactor LSP client to use unified provider-based capability lookup for diagnostics and other features. - Introduce `_provider_value_get` to abstract capability retrieval, supporting both static and dynamic registrations. - Update diagnostic handling and protocol mappings to leverage provider-centric logic. --- runtime/lua/vim/lsp/client.lua | 62 +++++-- runtime/lua/vim/lsp/diagnostic.lua | 30 ++-- runtime/lua/vim/lsp/protocol.lua | 159 ++++++++++-------- src/gen/gen_lsp.lua | 85 +++++++++- .../functional/plugin/lsp/diagnostic_spec.lua | 9 + test/functional/plugin/lsp_spec.lua | 101 +++++++++-- 6 files changed, 329 insertions(+), 117 deletions(-) diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 92c2dfb5d5..d98f1c000f 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -615,19 +615,17 @@ end function Client:_process_static_registrations() local static_registrations = {} ---@type lsp.Registration[] - for method, capability in pairs(lsp.protocol._request_name_to_server_capability) do + 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, unpack(capability), 'id') - --- @cast method vim.lsp.protocol.Method + vim.tbl_get(self.server_capabilities, capability[1], 'id') and self:_supports_registration(method) then - local cap = vim.tbl_get(self.server_capabilities, unpack(capability)) + local cap = vim.tbl_get(self.server_capabilities, capability[1]) static_registrations[#static_registrations + 1] = { id = cap.id, method = method, - registerOptions = { - documentSelector = cap.documentSelector, ---@type lsp.DocumentSelector|lsp.null - }, + registerOptions = cap or {}, } end end @@ -936,9 +934,12 @@ end --- Get options for a method that is registered dynamically. --- @param method vim.lsp.protocol.Method | vim.lsp.protocol.Method.Registration function Client:_supports_registration(method) - local capability_path = lsp.protocol._request_name_to_client_capability[method] or {} - -- dynamicRegistration is at the second level, even in deeply nested capabilities - local capability = vim.tbl_get(self.capabilities, capability_path[1], capability_path[2]) + if lsp.protocol._methods_with_no_registration_options[method] then + return true + end + local provider = self:_registration_provider(method) + local capability_path = lsp.protocol._provider_to_client_registration[provider] + local capability = vim.tbl_get(self.capabilities, unpack(capability_path)) return type(capability) == 'table' and capability.dynamicRegistration end @@ -946,7 +947,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] or method + return capability_path and capability_path[1] end --- @private @@ -1205,7 +1206,7 @@ function Client:supports_method(method, bufnr) local provider = self:_registration_provider(method) local regs = self:_get_registrations(provider, bufnr) - if lsp.protocol._request_name_allows_registration[method] and not regs then + if lsp.protocol._method_supports_dynamic_registration[method] and not regs then return false end if regs then @@ -1214,6 +1215,9 @@ function Client:supports_method(method, bufnr) if vim.tbl_get(reg, 'registerOptions', unpack(required_capability, 2)) then return self:_supports_registration(reg.method) end + if lsp.protocol._methods_with_no_registration_options[method] then + return true + end else return self:_supports_registration(reg.method) end @@ -1226,6 +1230,40 @@ function Client:supports_method(method, bufnr) return required_capability == nil end +--- Retrieves all capability values for a given LSP method, handling both static and dynamic registrations. +--- This function abstracts over differences between capabilities declared in `server_capabilities` +--- and those registered dynamically at runtime, returning all matching capability values. +--- It also handles cases where the registration method differs from the calling method by abstracting to the Provider. +--- For example, `workspace/diagnostic` uses capabilities registered under `textDocument/diagnostic`. +--- This is useful for features like diagnostics and formatting, where servers may register multiple providers +--- with different options (such as specific filetypes or document selectors). +--- @param method vim.lsp.protocol.Method.ClientToServer | vim.lsp.protocol.Method.Registration LSP method name +--- @param ... any Additional keys to index into the capability +--- @return lsp.LSPAny[] # The capability value if it exists, empty table if not found +function Client:_provider_value_get(method, ...) + local matched_regs = {} --- @type any[] + local provider = self:_registration_provider(method) + local dynamic_regs = self:_get_registrations(provider) + if not provider then + return matched_regs + elseif not dynamic_regs then + -- First check static capabilities + local static_reg = vim.tbl_get(self.server_capabilities, provider) + if static_reg then + matched_regs[1] = vim.tbl_get(static_reg, ...) or vim.NIL + end + else + local required_capability = lsp.protocol._request_name_to_server_capability[method] + for _, reg in ipairs(dynamic_regs) do + if vim.tbl_get(reg, 'registerOptions', unpack(required_capability, 2)) then + matched_regs[#matched_regs + 1] = vim.tbl_get(reg, 'registerOptions', ...) or vim.NIL + end + end + end + + return matched_regs +end + --- @private --- Handles a notification sent by an LSP server by invoking the --- corresponding handler. diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 92ae71a9c3..b4159a0ac7 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -193,14 +193,8 @@ function M.get_namespace(client_id, is_pull) local client = lsp.get_client_by_id(client_id) if is_pull then - local server_id = - vim.tbl_get((client or {}).server_capabilities or {}, 'diagnosticProvider', 'identifier') - local key = ('%d:%s'):format(client_id, server_id or 'nil') - local name = ('nvim.lsp.%s.%d.%s'):format( - client and client.name or 'unknown', - client_id, - server_id or 'nil' - ) + local key = ('%d'):format(client_id) + local name = ('nvim.lsp.%s.%d'):format(client and client.name or 'unknown', client_id) local ns = client_pull_namespaces[key] if not ns then ns = api.nvim_create_namespace(name) @@ -394,10 +388,7 @@ function M.on_refresh(err, _, ctx) if client == nil then return vim.NIL end - if - client.server_capabilities.diagnosticProvider - and client.server_capabilities.diagnosticProvider.workspaceDiagnostics - then + if client:supports_method('workspace/diagnostic') then M._workspace_diagnostics({ client_id = ctx.client_id }) else for bufnr in pairs(client.attached_buffers or {}) do @@ -532,13 +523,16 @@ function M._workspace_diagnostics(opts) end for _, client in ipairs(clients) do - --- @type lsp.WorkspaceDiagnosticParams - local params = { - identifier = vim.tbl_get(client, 'server_capabilities', 'diagnosticProvider', 'identifier'), - previousResultIds = previous_result_ids(client.id), - } + local identifiers = client:_provider_value_get('workspace/diagnostic', 'identifier') + for _, id in ipairs(identifiers) do + --- @type lsp.WorkspaceDiagnosticParams + local params = { + identifier = type(id) == 'string' and id or nil, + previousResultIds = previous_result_ids(client.id), + } - client:request('workspace/diagnostic', params, handler) + client:request('workspace/diagnostic', params, handler) + end end end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 3aa9325d47..979acb5db8 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -613,6 +613,15 @@ function protocol.make_client_capabilities() diagnostics = { refreshSupport = true, }, + fileOperations = { + dynamicRegistration = false, + didCreate = false, + willCreate = false, + didRename = false, + willRename = false, + didDelete = false, + willDelete = false, + }, }, experimental = nil, window = { @@ -1160,79 +1169,49 @@ protocol.Methods = { -- stylua: ignore start -- Generated by gen_lsp.lua, keep at end of file. --- Maps method names to the required client capability -protocol._request_name_to_client_capability = { - ['codeAction/resolve'] = { 'textDocument', 'codeAction', 'resolveSupport' }, - ['codeLens/resolve'] = { 'textDocument', 'codeLens', 'resolveSupport' }, - ['completionItem/resolve'] = { 'textDocument', 'completion', 'completionItem', 'resolveSupport' }, - ['documentLink/resolve'] = { 'textDocument', 'documentLink' }, - ['inlayHint/resolve'] = { 'textDocument', 'inlayHint', 'resolveSupport' }, - ['textDocument/codeAction'] = { 'textDocument', 'codeAction' }, - ['textDocument/codeLens'] = { 'textDocument', 'codeLens' }, - ['textDocument/colorPresentation'] = { 'textDocument', 'colorProvider' }, - ['textDocument/completion'] = { 'textDocument', 'completion' }, - ['textDocument/declaration'] = { 'textDocument', 'declaration' }, - ['textDocument/definition'] = { 'textDocument', 'definition' }, - ['textDocument/diagnostic'] = { 'textDocument', 'diagnostic' }, - ['textDocument/didChange'] = { 'textDocument', 'synchronization' }, - ['textDocument/didClose'] = { 'textDocument', 'synchronization' }, - ['textDocument/didOpen'] = { 'textDocument', 'synchronization' }, - ['textDocument/didSave'] = { 'textDocument', 'synchronization', 'didSave' }, - ['textDocument/documentColor'] = { 'textDocument', 'colorProvider' }, - ['textDocument/documentHighlight'] = { 'textDocument', 'documentHighlight' }, - ['textDocument/documentLink'] = { 'textDocument', 'documentLink' }, - ['textDocument/documentSymbol'] = { 'textDocument', 'documentSymbol' }, - ['textDocument/foldingRange'] = { 'textDocument', 'foldingRange' }, - ['textDocument/formatting'] = { 'textDocument', 'formatting' }, - ['textDocument/hover'] = { 'textDocument', 'hover' }, - ['textDocument/implementation'] = { 'textDocument', 'implementation' }, - ['textDocument/inlayHint'] = { 'textDocument', 'inlayHint' }, - ['textDocument/inlineCompletion'] = { 'textDocument', 'inlineCompletion' }, - ['textDocument/inlineValue'] = { 'textDocument', 'inlineValue' }, - ['textDocument/linkedEditingRange'] = { 'textDocument', 'linkedEditingRange' }, - ['textDocument/moniker'] = { 'textDocument', 'moniker' }, - ['textDocument/onTypeFormatting'] = { 'textDocument', 'onTypeFormatting' }, - ['textDocument/prepareCallHierarchy'] = { 'textDocument', 'callHierarchy' }, - ['textDocument/prepareRename'] = { 'textDocument', 'rename', 'prepareSupport' }, - ['textDocument/prepareTypeHierarchy'] = { 'textDocument', 'typeHierarchy' }, - ['textDocument/publishDiagnostics'] = { 'textDocument', 'publishDiagnostics' }, - ['textDocument/rangeFormatting'] = { 'textDocument', 'rangeFormatting' }, - ['textDocument/rangesFormatting'] = { 'textDocument', 'rangeFormatting', 'rangesSupport' }, - ['textDocument/references'] = { 'textDocument', 'references' }, - ['textDocument/rename'] = { 'textDocument', 'rename' }, - ['textDocument/selectionRange'] = { 'textDocument', 'selectionRange' }, - ['textDocument/semanticTokens/full'] = { 'textDocument', 'semanticTokens' }, - ['textDocument/semanticTokens/full/delta'] = { 'textDocument', 'semanticTokens', 'requests', 'full', 'delta' }, - ['textDocument/semanticTokens/range'] = { 'textDocument', 'semanticTokens', 'requests', 'range' }, - ['textDocument/signatureHelp'] = { 'textDocument', 'signatureHelp' }, - ['textDocument/typeDefinition'] = { 'textDocument', 'typeDefinition' }, - ['textDocument/willSave'] = { 'textDocument', 'synchronization', 'willSave' }, - ['textDocument/willSaveWaitUntil'] = { 'textDocument', 'synchronization', 'willSaveWaitUntil' }, - ['window/showDocument'] = { 'window', 'showDocument', 'support' }, - ['window/showMessage'] = { 'window', 'showMessage' }, - ['window/showMessageRequest'] = { 'window', 'showMessage' }, - ['window/workDoneProgress/create'] = { 'window', 'workDoneProgress' }, - ['workspaceSymbol/resolve'] = { 'workspace', 'symbol', 'resolveSupport' }, - ['workspace/applyEdit'] = { 'workspace', 'applyEdit' }, - ['workspace/codeLens/refresh'] = { 'workspace', 'codeLens' }, - ['workspace/configuration'] = { 'workspace', 'configuration' }, - ['workspace/diagnostic'] = { 'workspace', 'diagnostics' }, - ['workspace/diagnostic/refresh'] = { 'workspace', 'diagnostics', 'refreshSupport' }, +---TODO: also has workspace/* items because spec lacks a top-level "workspaceProvider" +protocol._provider_to_client_registration = { + ['callHierarchyProvider'] = { 'textDocument', 'callHierarchy' }, + ['codeActionProvider'] = { 'textDocument', 'codeAction' }, + ['codeLensProvider'] = { 'textDocument', 'codeLens' }, + ['colorProvider'] = { 'textDocument', 'colorProvider' }, + ['completionProvider'] = { 'textDocument', 'completion' }, + ['declarationProvider'] = { 'textDocument', 'declaration' }, + ['definitionProvider'] = { 'textDocument', 'definition' }, + ['diagnosticProvider'] = { 'textDocument', 'diagnostic' }, + ['documentFormattingProvider'] = { 'textDocument', 'formatting' }, + ['documentHighlightProvider'] = { 'textDocument', 'documentHighlight' }, + ['documentLinkProvider'] = { 'textDocument', 'documentLink' }, + ['documentOnTypeFormattingProvider'] = { 'textDocument', 'onTypeFormatting' }, + ['documentRangeFormattingProvider'] = { 'textDocument', 'rangeFormatting' }, + ['documentSymbolProvider'] = { 'textDocument', 'documentSymbol' }, + ['executeCommandProvider'] = { 'workspace', 'executeCommand' }, + ['foldingRangeProvider'] = { 'textDocument', 'foldingRange' }, + ['hoverProvider'] = { 'textDocument', 'hover' }, + ['implementationProvider'] = { 'textDocument', 'implementation' }, + ['inlayHintProvider'] = { 'textDocument', 'inlayHint' }, + ['inlineCompletionProvider'] = { 'textDocument', 'inlineCompletion' }, + ['inlineValueProvider'] = { 'textDocument', 'inlineValue' }, + ['linkedEditingRangeProvider'] = { 'textDocument', 'linkedEditingRange' }, + ['monikerProvider'] = { 'textDocument', 'moniker' }, + ['referencesProvider'] = { 'textDocument', 'references' }, + ['renameProvider'] = { 'textDocument', 'rename' }, + ['selectionRangeProvider'] = { 'textDocument', 'selectionRange' }, + ['semanticTokensProvider'] = { 'textDocument', 'semanticTokens' }, + ['signatureHelpProvider'] = { 'textDocument', 'signatureHelp' }, + ['textDocumentSync'] = { 'textDocument', 'synchronization' }, + ['typeDefinitionProvider'] = { 'textDocument', 'typeDefinition' }, + ['typeHierarchyProvider'] = { 'textDocument', 'typeHierarchy' }, ['workspace/didChangeConfiguration'] = { 'workspace', 'didChangeConfiguration' }, ['workspace/didChangeWatchedFiles'] = { 'workspace', 'didChangeWatchedFiles' }, ['workspace/didCreateFiles'] = { 'workspace', 'fileOperations', 'didCreate' }, ['workspace/didDeleteFiles'] = { 'workspace', 'fileOperations', 'didDelete' }, ['workspace/didRenameFiles'] = { 'workspace', 'fileOperations', 'didRename' }, - ['workspace/executeCommand'] = { 'workspace', 'executeCommand' }, - ['workspace/foldingRange/refresh'] = { 'workspace', 'foldingRange', 'refreshSupport' }, - ['workspace/inlayHint/refresh'] = { 'workspace', 'inlayHint', 'refreshSupport' }, - ['workspace/inlineValue/refresh'] = { 'workspace', 'inlineValue', 'refreshSupport' }, - ['workspace/semanticTokens/refresh'] = { 'workspace', 'semanticTokens', 'refreshSupport' }, - ['workspace/symbol'] = { 'workspace', 'symbol' }, ['workspace/textDocumentContent'] = { 'workspace', 'textDocumentContent' }, ['workspace/willCreateFiles'] = { 'workspace', 'fileOperations', 'willCreate' }, ['workspace/willDeleteFiles'] = { 'workspace', 'fileOperations', 'willDelete' }, ['workspace/willRenameFiles'] = { 'workspace', 'fileOperations', 'willRename' }, - ['workspace/workspaceFolders'] = { 'workspace', 'workspaceFolders' }, + ['workspaceSymbolProvider'] = { 'workspace', 'symbol' }, } -- stylua: ignore end @@ -1299,13 +1278,13 @@ protocol._request_name_to_server_capability = { ['workspace/willRenameFiles'] = { 'workspace', 'fileOperations', 'willRename' }, ['workspace/workspaceFolders'] = { 'workspace', 'workspaceFolders' }, ['textDocument/semanticTokens'] = { 'semanticTokensProvider' }, + ['workspace/didChangeWatchedFiles'] = { 'workspace/didChangeWatchedFiles' }, } -- stylua: ignore end -- stylua: ignore start -- Generated by gen_lsp.lua, keep at end of file. ---- Maps method names to the required client capability -protocol._request_name_allows_registration = { +protocol._method_supports_dynamic_registration = { ['notebookDocument/didChange'] = true, ['notebookDocument/didClose'] = true, ['notebookDocument/didOpen'] = true, @@ -1351,6 +1330,7 @@ protocol._request_name_allows_registration = { ['textDocument/willSaveWaitUntil'] = true, ['workspace/didChangeConfiguration'] = true, ['workspace/didChangeWatchedFiles'] = true, + ['workspace/didChangeWorkspaceFolders'] = true, ['workspace/didCreateFiles'] = true, ['workspace/didDeleteFiles'] = true, ['workspace/didRenameFiles'] = true, @@ -1363,4 +1343,51 @@ protocol._request_name_allows_registration = { } -- stylua: ignore end +-- stylua: ignore start +-- Generated by gen_lsp.lua, keep at end of file. +protocol._method_supports_static_registration = { + ['textDocument/codeAction'] = true, + ['textDocument/codeLens'] = 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/typeDefinition'] = true, + ['workspace/executeCommand'] = true, + ['workspace/symbol'] = true, +} +-- stylua: ignore end + +-- stylua: ignore start +-- Generated by gen_lsp.lua, keep at end of file. +-- These methods have no registration options but can still be registered dynamically. +protocol._methods_with_no_registration_options = { + ['workspace/didChangeWorkspaceFolders'] = true , +} +-- stylua: ignore end + return protocol diff --git a/src/gen/gen_lsp.lua b/src/gen/gen_lsp.lua index 4d8aca9510..c4af75f535 100755 --- a/src/gen/gen_lsp.lua +++ b/src/gen/gen_lsp.lua @@ -224,18 +224,37 @@ 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', - 'protocol._request_name_to_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 - if item.clientCapability then - output[#output + 1] = (" ['%s'] = { %s },"):format( - item.method, - "'" .. item.clientCapability:gsub('%.', "', '") .. "'" - ) + 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 + end + local key = base_provider or item.method + providers[key] = item.clientCapability end end + ---@type { provider: string, path : string }[] + local found_entries = {} + for key, value in pairs(providers) do + found_entries[#found_entries + 1] = { provider = key, path = value } + end + table.sort(found_entries, function(a, b) + return a.provider < b.provider + end) + for _, entry in ipairs(found_entries) do + output[#output + 1] = (" ['%s'] = { %s },"):format( + entry.provider, + "'" .. entry.path:gsub('%.', "', '") .. "'" + ) + end + output[#output + 1] = '}' output[#output + 1] = '-- stylua: ignore end' @@ -280,6 +299,13 @@ local function write_to_vim_protocol(protocol) ) end + --- workspace/didChangeWatchedFiles has no server capability but we need to map it for + --- registration + output[#output + 1] = (" ['%s'] = { '%s' },"):format( + 'workspace/didChangeWatchedFiles', + 'workspace/didChangeWatchedFiles' + ) + output[#output + 1] = '}' output[#output + 1] = '-- stylua: ignore end' @@ -287,18 +313,59 @@ 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', - 'protocol._request_name_allows_registration = {', + 'protocol._method_supports_dynamic_registration = {', }) + --- These methods have no registrationOptions but can still be registered + --- TODO: remove if resolved upstream: https://github.com/microsoft/language-server-protocol/issues/2218 + local methods_with_no_registration_options = { + ['workspace/didChangeWorkspaceFolders'] = true, + } + for _, item in ipairs(all) do - if item.registrationMethod or item.registrationOptions then + if + item.registrationMethod + or item.registrationOptions + or methods_with_no_registration_options[item.method] + then output[#output + 1] = (" ['%s'] = %s,"):format(item.method, true) end end 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.', + 'protocol._method_supports_static_registration = {', + }) + + for _, item in ipairs(all) do + if + item.registrationOptions + and (item.serverCapability and not item.serverCapability:find('%.')) + then + output[#output + 1] = (" ['%s'] = %s,"):format(item.method, true) + end + end + + 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.', + '-- These methods have no registration options but can still be registered dynamically.', + 'protocol._methods_with_no_registration_options = {', + }) + for key, v in pairs(methods_with_no_registration_options) do + output[#output + 1] = (" ['%s'] = %s ,"):format(key, v) + end + output[#output + 1] = '}' + output[#output + 1] = '-- stylua: ignore end' end output[#output + 1] = '' diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index ffaf2dcdbf..6952f9fdb1 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -774,6 +774,15 @@ describe('vim.lsp.diagnostic', function() end) ) + eq( + { vim.NIL }, + exec_lua(function() + local client = vim.lsp.get_client_by_id(client_id) + assert(client) + return client:_provider_value_get('workspace/diagnostic', 'identifier') + end) + ) + local requests, diags = exec_lua(function() vim.lsp.diagnostic.on_refresh(nil, nil, { method = 'workspace/diagnostic/refresh', diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index af5f9d7b5c..891aed0028 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -5688,13 +5688,14 @@ describe('LSP', function() }, { client_id = client_id }) local result = {} - local function check(method, fname) + local function check(method, fname, ...) local bufnr = fname and vim.fn.bufadd(fname) or nil local client = assert(vim.lsp.get_client_by_id(client_id)) result[#result + 1] = { method = method, fname = fname, - supported = client:supports_method(method, { bufnr = bufnr }), + supported = client:supports_method(method, bufnr), + cap = select('#', ...) > 0 and client:_provider_value_get(method, ...) or nil, } end @@ -5736,6 +5737,7 @@ describe('LSP', function() id = 'diag1', method = 'textDocument/diagnostic', registerOptions = { + identifier = 'diag-ident-1', -- workspaceDiagnostics field omitted }, }, @@ -5744,7 +5746,7 @@ describe('LSP', function() -- Checks after registering without workspaceDiagnostics support -- Returns false - check('workspace/diagnostic') + check('workspace/diagnostic', nil, 'identifier') vim.lsp.handlers['client/registerCapability'](nil, { registrations = { @@ -5752,6 +5754,7 @@ describe('LSP', function() id = 'diag2', method = 'textDocument/diagnostic', registerOptions = { + identifier = 'diag-ident-2', workspaceDiagnostics = true, }, }, @@ -5760,7 +5763,7 @@ describe('LSP', function() -- Check after second registration with support -- Returns true - check('workspace/diagnostic') + check('workspace/diagnostic', nil, 'identifier') vim.lsp.handlers['client/unregisterCapability'](nil, { unregisterations = { @@ -5770,7 +5773,7 @@ describe('LSP', function() -- Check after unregistering -- Returns false - check('workspace/diagnostic') + check('workspace/diagnostic', nil, 'identifier') check('textDocument/codeAction') check('codeAction/resolve') @@ -5790,10 +5793,21 @@ describe('LSP', function() check('textDocument/codeAction') check('codeAction/resolve') + check('workspace/didChangeWorkspaceFolders') + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'didChangeWorkspaceFolders-id', + method = 'workspace/didChangeWorkspaceFolders', + }, + }, + }, { client_id = client_id }) + check('workspace/didChangeWorkspaceFolders') + return result end) - eq(17, #result) + eq(19, #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]) @@ -5810,13 +5824,19 @@ describe('LSP', function() result[9] ) eq({ method = 'workspace/diagnostic', supported = false }, result[10]) - eq({ method = 'workspace/diagnostic', supported = false }, result[11]) - eq({ method = 'workspace/diagnostic', supported = true }, result[12]) - eq({ method = 'workspace/diagnostic', supported = false }, result[13]) + eq({ method = 'workspace/diagnostic', supported = false, cap = {} }, result[11]) + eq({ + method = 'workspace/diagnostic', + supported = true, + cap = { 'diag-ident-2' }, + }, result[12]) + eq({ method = 'workspace/diagnostic', supported = false, cap = {} }, result[13]) eq({ method = 'textDocument/codeAction', supported = false }, result[14]) eq({ method = 'codeAction/resolve', supported = false }, result[15]) eq({ method = 'textDocument/codeAction', supported = true }, result[16]) eq({ method = 'codeAction/resolve', supported = true }, result[17]) + eq({ method = 'workspace/didChangeWorkspaceFolders', supported = false }, result[18]) + eq({ method = 'workspace/didChangeWorkspaceFolders', supported = true }, result[19]) end) it('identifies client dynamic registration capability', function() @@ -5834,6 +5854,9 @@ describe('LSP', function() synchronization = { dynamicRegistration = true, }, + diagnostic = { + dynamicRegistration = true, + }, }, }, })) @@ -5851,15 +5874,19 @@ describe('LSP', function() check('textDocument/didSave') check('textDocument/didOpen') check('textDocument/codeLens') + check('textDocument/diagnostic') + check('workspace/diagnostic') return result end) - eq(4, #result) + eq(6, #result) eq({ method = 'textDocument/formatting', supports_reg = true }, result[1]) eq({ method = 'textDocument/didSave', supports_reg = true }, result[2]) eq({ method = 'textDocument/didOpen', supports_reg = true }, result[3]) eq({ method = 'textDocument/codeLens', supports_reg = false }, result[4]) + eq({ method = 'textDocument/diagnostic', supports_reg = true }, result[5]) + eq({ method = 'workspace/diagnostic', supports_reg = true }, result[6]) end) it('supports static registration', function() @@ -5869,17 +5896,67 @@ describe('LSP', function() local server = _G._create_server({ capabilities = { colorProvider = { id = 'color-registration' }, + diagnosticProvider = { + id = 'diag-registration', + identifier = 'diag-ident-static', + workspaceDiagnostics = true, + }, }, }) return assert(vim.lsp.start({ name = 'dynamic-test', cmd = server.cmd })) end) + local function sort_method(tbl) + local result_t = vim.deepcopy(tbl) + table.sort(result_t, function(a, b) + return (a.method or '') < (b.method or '') + end) + return result_t + end + eq( - true, + { + { + id = 'color-registration', + method = 'textDocument/colorPresentation', + registerOptions = { id = 'color-registration' }, + }, + { + id = 'color-registration', + method = 'textDocument/documentColor', + registerOptions = { id = 'color-registration' }, + }, + }, + sort_method(exec_lua(function() + local client = assert(vim.lsp.get_client_by_id(client_id)) + return client.dynamic_capabilities:get('colorProvider') + end)) + ) + + eq( + { + { + id = 'diag-registration', + method = 'textDocument/diagnostic', + registerOptions = { + id = 'diag-registration', + identifier = 'diag-ident-static', + workspaceDiagnostics = true, + }, + }, + }, + sort_method(exec_lua(function() + local client = assert(vim.lsp.get_client_by_id(client_id)) + return client.dynamic_capabilities:get('diagnosticProvider') + end)) + ) + + eq( + { 'diag-ident-static' }, exec_lua(function() local client = assert(vim.lsp.get_client_by_id(client_id)) - return client.dynamic_capabilities:get('colorProvider') ~= nil + return client:_provider_value_get('textDocument/diagnostic', 'identifier') end) ) end)