aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/news.txt1
-rw-r--r--runtime/lua/vim/lsp.lua121
-rw-r--r--runtime/lua/vim/lsp/_dynamic.lua109
-rw-r--r--runtime/lua/vim/lsp/buf.lua6
-rw-r--r--runtime/lua/vim/lsp/handlers.lua28
-rw-r--r--runtime/lua/vim/lsp/protocol.lua13
-rw-r--r--runtime/lua/vim/lsp/types.lua28
-rw-r--r--test/functional/plugin/lsp_spec.lua90
8 files changed, 327 insertions, 69 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 795ccc55de..b6839ec692 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -36,6 +36,7 @@ ADDED FEATURES *news-added*
The following new APIs or features were added.
+• Dynamic registration of LSP capabilities. An implication of this change is that checking a client's `server_capabilities` is no longer a sufficient indicator to see if a server supports a feature. Instead use `client.supports_method(<method>)`. It considers both the dynamic capabilities and static `server_capabilities`.
• |vim.iter()| provides a generic iterator interface for tables and Lua
iterators |luaref-in|.
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 2e6ca7a0ac..5337abea25 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -50,6 +50,7 @@ lsp._request_name_to_capability = {
['textDocument/codeAction'] = { 'codeActionProvider' },
['textDocument/codeLens'] = { 'codeLensProvider' },
['codeLens/resolve'] = { 'codeLensProvider', 'resolveProvider' },
+ ['codeAction/resolve'] = { 'codeActionProvider', 'resolveProvider' },
['workspace/executeCommand'] = { 'executeCommandProvider' },
['workspace/symbol'] = { 'workspaceSymbolProvider' },
['textDocument/references'] = { 'referencesProvider' },
@@ -886,6 +887,47 @@ function lsp.start(config, opts)
return client_id
end
+---@private
+-- Determines whether the given option can be set by `set_defaults`.
+local function is_empty_or_default(bufnr, option)
+ if vim.bo[bufnr][option] == '' then
+ return true
+ end
+
+ local info = vim.api.nvim_get_option_info2(option, { buf = bufnr })
+ local scriptinfo = vim.tbl_filter(function(e)
+ return e.sid == info.last_set_sid
+ end, vim.fn.getscriptinfo())
+
+ if #scriptinfo ~= 1 then
+ return false
+ end
+
+ return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME'))
+end
+
+---@private
+---@param client lsp.Client
+function lsp._set_defaults(client, bufnr)
+ if
+ client.supports_method('textDocument/definition') and is_empty_or_default(bufnr, 'tagfunc')
+ then
+ vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
+ end
+ if
+ client.supports_method('textDocument/completion') and is_empty_or_default(bufnr, 'omnifunc')
+ then
+ vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
+ end
+ if
+ client.supports_method('textDocument/rangeFormatting')
+ and is_empty_or_default(bufnr, 'formatprg')
+ and is_empty_or_default(bufnr, 'formatexpr')
+ then
+ vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
+ end
+end
+
-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are
-- documented twice: Here, and on the methods themselves (e.g.
-- `client.request()`). This is a workaround for the vimdoc generator script
@@ -1091,43 +1133,6 @@ function lsp.start_client(config)
end
---@private
- -- Determines whether the given option can be set by `set_defaults`.
- local function is_empty_or_default(bufnr, option)
- if vim.bo[bufnr][option] == '' then
- return true
- end
-
- local info = vim.api.nvim_get_option_info2(option, { buf = bufnr })
- local scriptinfo = vim.tbl_filter(function(e)
- return e.sid == info.last_set_sid
- end, vim.fn.getscriptinfo())
-
- if #scriptinfo ~= 1 then
- return false
- end
-
- return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME'))
- end
-
- ---@private
- local function set_defaults(client, bufnr)
- local capabilities = client.server_capabilities
- if capabilities.definitionProvider and is_empty_or_default(bufnr, 'tagfunc') then
- vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
- end
- if capabilities.completionProvider and is_empty_or_default(bufnr, 'omnifunc') then
- vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
- end
- if
- capabilities.documentRangeFormattingProvider
- and is_empty_or_default(bufnr, 'formatprg')
- and is_empty_or_default(bufnr, 'formatexpr')
- then
- vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
- end
- end
-
- ---@private
--- Reset defaults set by `set_defaults`.
--- Must only be called if the last client attached to a buffer exits.
local function unset_defaults(bufnr)
@@ -1228,7 +1233,9 @@ function lsp.start_client(config)
requests = {},
-- for $/progress report
messages = { name = name, messages = {}, progress = {}, status = {} },
+ dynamic_capabilities = require('vim.lsp._dynamic').new(client_id),
}
+ client.config.capabilities = config.capabilities or protocol.make_client_capabilities()
-- Store the uninitialized_clients for cleanup in case we exit before initialize finishes.
uninitialized_clients[client_id] = client
@@ -1291,7 +1298,7 @@ function lsp.start_client(config)
-- User provided initialization options.
initializationOptions = config.init_options,
-- The capabilities provided by the client (editor or tool)
- capabilities = config.capabilities or protocol.make_client_capabilities(),
+ capabilities = config.capabilities,
-- The initial trace setting. If omitted trace is disabled ("off").
-- trace = "off" | "messages" | "verbose";
trace = valid_traces[config.trace] or 'off',
@@ -1300,6 +1307,26 @@ function lsp.start_client(config)
-- TODO(ashkan) handle errors here.
pcall(config.before_init, initialize_params, config)
end
+
+ --- @param method string
+ --- @param opts? {bufnr?: number}
+ client.supports_method = function(method, opts)
+ opts = opts or {}
+ local required_capability = lsp._request_name_to_capability[method]
+ -- if we don't know about the method, assume that the client supports it.
+ if not required_capability then
+ return true
+ end
+ if vim.tbl_get(client.server_capabilities or {}, unpack(required_capability)) then
+ return true
+ else
+ if client.dynamic_capabilities:supports_registration(method) then
+ return client.dynamic_capabilities:supports(method, opts)
+ end
+ return false
+ end
+ end
+
local _ = log.trace() and log.trace(log_prefix, 'initialize_params', initialize_params)
rpc.request('initialize', initialize_params, function(init_err, result)
assert(not init_err, tostring(init_err))
@@ -1314,18 +1341,6 @@ function lsp.start_client(config)
client.server_capabilities =
assert(result.capabilities, "initialize result doesn't contain capabilities")
client.server_capabilities = protocol.resolve_capabilities(client.server_capabilities)
- client.supports_method = function(method)
- local required_capability = lsp._request_name_to_capability[method]
- -- if we don't know about the method, assume that the client supports it.
- if not required_capability then
- return true
- end
- if vim.tbl_get(client.server_capabilities, unpack(required_capability)) then
- return true
- else
- return false
- end
- end
if next(config.settings) then
client.notify('workspace/didChangeConfiguration', { settings = config.settings })
@@ -1522,7 +1537,7 @@ function lsp.start_client(config)
function client._on_attach(bufnr)
text_document_did_open_handler(bufnr, client)
- set_defaults(client, bufnr)
+ lsp._set_defaults(client, bufnr)
nvim_exec_autocmds('LspAttach', {
buffer = bufnr,
@@ -1946,7 +1961,7 @@ function lsp.buf_request(bufnr, method, params, handler)
local supported_clients = {}
local method_supported = false
for_each_buffer_client(bufnr, function(client, client_id)
- if client.supports_method(method) then
+ if client.supports_method(method, { bufnr = bufnr }) then
method_supported = true
table.insert(supported_clients, client_id)
end
@@ -2002,7 +2017,7 @@ function lsp.buf_request_all(bufnr, method, params, callback)
local set_expected_result_count = once(function()
for_each_buffer_client(bufnr, function(client)
- if client.supports_method(method) then
+ if client.supports_method(method, { bufnr = bufnr }) then
expected_result_count = expected_result_count + 1
end
end)
diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua
new file mode 100644
index 0000000000..04040e8e28
--- /dev/null
+++ b/runtime/lua/vim/lsp/_dynamic.lua
@@ -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
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index a307dea673..b2f202c4ba 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -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)
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 8e926c4644..5346160871 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -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)
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index a7919f12f5..a28ff407b7 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -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 = {
diff --git a/runtime/lua/vim/lsp/types.lua b/runtime/lua/vim/lsp/types.lua
index 779f313aa7..e77e1fb63a 100644
--- a/runtime/lua/vim/lsp/types.lua
+++ b/runtime/lua/vim/lsp/types.lua
@@ -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[]}
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index b906ae265f..1a7a656d1d 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -3765,6 +3765,96 @@ describe('LSP', function()
end)
end)
+ describe('#dynamic vim.lsp._dynamic', function()
+ it('supports dynamic registration', function()
+ local root_dir = helpers.tmpname()
+ os.remove(root_dir)
+ mkdir(root_dir)
+ local tmpfile = root_dir .. '/dynamic.foo'
+ local file = io.open(tmpfile, 'w')
+ file:close()
+
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local root_dir, tmpfile = ...
+
+ local server = _create_server()
+ local client_id = vim.lsp.start({
+ name = 'dynamic-test',
+ cmd = server.cmd,
+ root_dir = root_dir,
+ capabilities = {
+ textDocument = {
+ formatting = {
+ dynamicRegistration = true,
+ },
+ rangeFormatting = {
+ dynamicRegistration = true,
+ },
+ },
+ },
+ })
+
+ local expected_messages = 2 -- initialize, initialized
+
+ vim.lsp.handlers['client/registerCapability'](nil, {
+ registrations = {
+ {
+ id = 'formatting',
+ method = 'textDocument/formatting',
+ registerOptions = {
+ documentSelector = {{
+ pattern = root_dir .. '/*.foo',
+ }},
+ },
+ },
+ },
+ }, { client_id = client_id })
+
+ vim.lsp.handlers['client/registerCapability'](nil, {
+ registrations = {
+ {
+ id = 'range-formatting',
+ method = 'textDocument/rangeFormatting',
+ },
+ },
+ }, { client_id = client_id })
+
+ vim.lsp.handlers['client/registerCapability'](nil, {
+ registrations = {
+ {
+ id = 'completion',
+ method = 'textDocument/completion',
+ },
+ },
+ }, { client_id = client_id })
+
+ local result = {}
+ local function check(method, fname)
+ local bufnr = fname and vim.fn.bufadd(fname) or nil
+ local client = vim.lsp.get_client_by_id(client_id)
+ result[#result + 1] = {method = method, fname = fname, supported = client.supports_method(method, {bufnr = bufnr})}
+ end
+
+
+ check("textDocument/formatting")
+ check("textDocument/formatting", tmpfile)
+ check("textDocument/rangeFormatting")
+ check("textDocument/rangeFormatting", tmpfile)
+ check("textDocument/completion")
+
+ return result
+ ]], root_dir, tmpfile)
+
+ eq(5, #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])
+ eq({method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile}, result[4])
+ eq({method = 'textDocument/completion', supported = false}, result[5])
+ end)
+ end)
+
describe('vim.lsp._watchfiles', function()
it('sends notifications when files change', function()
local root_dir = helpers.tmpname()