feat(lsp): initial support for dynamic capabilities (#23681)

- `client.dynamic_capabilities` is an object that tracks client register/unregister
- `client.supports_method` will additionally check if a dynamic capability supports the method, taking document filters into account. But only if the client enabled `dynamicRegistration` for the capability
- updated the default client capabilities to include dynamicRegistration for:
    - formatting
    - rangeFormatting
    - hover
    - codeAction
    - hover
    - rename
This commit is contained in:
Folke Lemaitre
2023-05-28 07:51:28 +02:00
committed by GitHub
parent e41b2e34b4
commit ddd92a70d2
8 changed files with 327 additions and 69 deletions

View File

@@ -0,0 +1,109 @@
local wf = require('vim.lsp._watchfiles')
--- @class lsp.DynamicCapabilities
--- @field capabilities table<string, lsp.Registration[]>
--- @field client_id number
local M = {}
--- @param client_id number
function M.new(client_id)
return setmetatable({
capabilities = {},
client_id = client_id,
}, { __index = M })
end
function M:supports_registration(method)
local client = vim.lsp.get_client_by_id(self.client_id)
if not client then
return false
end
local capability = vim.tbl_get(client.config.capabilities, unpack(vim.split(method, '/')))
return type(capability) == 'table' and capability.dynamicRegistration
end
--- @param registrations lsp.Registration[]
--- @private
function M:register(registrations)
-- remove duplicates
self:unregister(registrations)
for _, reg in ipairs(registrations) do
local method = reg.method
if not self.capabilities[method] then
self.capabilities[method] = {}
end
table.insert(self.capabilities[method], reg)
end
end
--- @param unregisterations lsp.Unregistration[]
--- @private
function M:unregister(unregisterations)
for _, unreg in ipairs(unregisterations) do
local method = unreg.method
if not self.capabilities[method] then
return
end
local id = unreg.id
for i, reg in ipairs(self.capabilities[method]) do
if reg.id == id then
table.remove(self.capabilities[method], i)
break
end
end
end
end
--- @param method string
--- @param opts? {bufnr?: number}
--- @return lsp.Registration? (table|nil) the registration if found
--- @private
function M:get(method, opts)
opts = opts or {}
opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf()
for _, reg in ipairs(self.capabilities[method] or {}) do
if not reg.registerOptions then
return reg
end
local documentSelector = reg.registerOptions.documentSelector
if not documentSelector then
return reg
end
if M.match(opts.bufnr, documentSelector) then
return reg
end
end
end
--- @param method string
--- @param opts? {bufnr?: number}
--- @private
function M:supports(method, opts)
return self:get(method, opts) ~= nil
end
--- @param bufnr number
--- @param documentSelector lsp.DocumentSelector
--- @private
function M.match(bufnr, documentSelector)
local ft = vim.bo[bufnr].filetype
local uri = vim.uri_from_bufnr(bufnr)
local fname = vim.uri_to_fname(uri)
for _, filter in ipairs(documentSelector) do
local matches = true
if filter.language and ft ~= filter.language then
matches = false
end
if matches and filter.scheme and not vim.startswith(uri, filter.scheme .. ':') then
matches = false
end
if matches and filter.pattern and not wf._match(filter.pattern, fname) then
matches = false
end
if matches then
return true
end
end
end
return M

View File

@@ -683,11 +683,7 @@ local function on_code_action_results(results, ctx, options)
--
local client = vim.lsp.get_client_by_id(action_tuple[1])
local action = action_tuple[2]
if
not action.edit
and client
and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider')
then
if not action.edit and client and client.supports_method('codeAction/resolve') then
client.request('codeAction/resolve', action, function(err, resolved_action)
if err then
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)

View File

@@ -118,22 +118,30 @@ end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
M['client/registerCapability'] = function(_, result, ctx)
local log_unsupported = false
local client_id = ctx.client_id
---@type lsp.Client
local client = vim.lsp.get_client_by_id(client_id)
client.dynamic_capabilities:register(result.registrations)
for bufnr, _ in ipairs(client.attached_buffers) do
vim.lsp._set_defaults(client, bufnr)
end
---@type string[]
local unsupported = {}
for _, reg in ipairs(result.registrations) do
if reg.method == 'workspace/didChangeWatchedFiles' then
require('vim.lsp._watchfiles').register(reg, ctx)
else
log_unsupported = true
elseif not client.dynamic_capabilities:supports_registration(reg.method) then
unsupported[#unsupported + 1] = reg.method
end
end
if log_unsupported then
local client_id = ctx.client_id
if #unsupported > 0 then
local warning_tpl = 'The language server %s triggers a registerCapability '
.. 'handler despite dynamicRegistration set to false. '
.. 'handler for %s despite dynamicRegistration set to false. '
.. 'Report upstream, this warning is harmless'
local client = vim.lsp.get_client_by_id(client_id)
local client_name = client and client.name or string.format('id=%d', client_id)
local warning = string.format(warning_tpl, client_name)
local warning = string.format(warning_tpl, client_name, table.concat(unsupported, ', '))
log.warn(warning)
end
return vim.NIL
@@ -141,6 +149,10 @@ end
--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability
M['client/unregisterCapability'] = function(_, result, ctx)
local client_id = ctx.client_id
local client = vim.lsp.get_client_by_id(client_id)
client.dynamic_capabilities:unregister(result.unregisterations)
for _, unreg in ipairs(result.unregisterations) do
if unreg.method == 'workspace/didChangeWatchedFiles' then
require('vim.lsp._watchfiles').unregister(unreg, ctx)

View File

@@ -697,7 +697,7 @@ function protocol.make_client_capabilities()
didSave = true,
},
codeAction = {
dynamicRegistration = false,
dynamicRegistration = true,
codeActionLiteralSupport = {
codeActionKind = {
@@ -714,6 +714,12 @@ function protocol.make_client_capabilities()
properties = { 'edit' },
},
},
formatting = {
dynamicRegistration = true,
},
rangeFormatting = {
dynamicRegistration = true,
},
completion = {
dynamicRegistration = false,
completionItem = {
@@ -747,6 +753,7 @@ function protocol.make_client_capabilities()
},
definition = {
linkSupport = true,
dynamicRegistration = true,
},
implementation = {
linkSupport = true,
@@ -755,7 +762,7 @@ function protocol.make_client_capabilities()
linkSupport = true,
},
hover = {
dynamicRegistration = false,
dynamicRegistration = true,
contentFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText },
},
signatureHelp = {
@@ -790,7 +797,7 @@ function protocol.make_client_capabilities()
hierarchicalDocumentSymbolSupport = true,
},
rename = {
dynamicRegistration = false,
dynamicRegistration = true,
prepareSupport = true,
},
publishDiagnostics = {

View File

@@ -35,3 +35,31 @@
---@field source string
---@field tags? lsp.DiagnosticTag[]
---@field relatedInformation DiagnosticRelatedInformation[]
--- @class lsp.DocumentFilter
--- @field language? string
--- @field scheme? string
--- @field pattern? string
--- @alias lsp.DocumentSelector lsp.DocumentFilter[]
--- @alias lsp.RegisterOptions any | lsp.StaticRegistrationOptions | lsp.TextDocumentRegistrationOptions
--- @class lsp.Registration
--- @field id string
--- @field method string
--- @field registerOptions? lsp.RegisterOptions
--- @alias lsp.RegistrationParams {registrations: lsp.Registration[]}
--- @class lsp.StaticRegistrationOptions
--- @field id? string
--- @class lsp.TextDocumentRegistrationOptions
--- @field documentSelector? lsp.DocumentSelector
--- @class lsp.Unregistration
--- @field id string
--- @field method string
--- @alias lsp.UnregistrationParams {unregisterations: lsp.Unregistration[]}