mirror of
https://github.com/neovim/neovim
synced 2025-07-16 09:11:51 +00:00
fix(lsp): do not assume client capability exists in watchfiles check (#24550)
PR #23689 assumes `client.config.capabilities.workspace.didChangeWatchedFiles` exists when checking `dynamicRegistration`, but thats's true only if it was passed to `vim.lsp.start{_client}`. This caused #23806 (still an issue in v0.9.1; needs manual backport), but #23681 fixed it by defaulting `config.capabilities` to `make_client_capabilities` if not passed to `vim.lsp.start{_client}`. However, the bug resurfaces on HEAD if you provide a non-nil `capabilities` to `vim.lsp.start{_client}` with missing fields (e.g: not made via `make_client_capabilities`). From what I see, the spec says such missing fields should be interpreted as an absence of the capability (including those indicated by missing sub-fields): https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities Also, suggest `vim.empty_dict()` for an empty dict in `:h vim.lsp.start_client()` (`{[vim.type_idx]=vim.types.dictionary}` no longer works anyway, probably since the cjson switch).
This commit is contained in:
@ -988,8 +988,7 @@ start_client({config}) *vim.lsp.start_client()*
|
|||||||
passed to the language server on initialization. Hint: use
|
passed to the language server on initialization. Hint: use
|
||||||
make_client_capabilities() and modify its result.
|
make_client_capabilities() and modify its result.
|
||||||
• Note: To send an empty dictionary use
|
• Note: To send an empty dictionary use
|
||||||
`{[vim.type_idx]=vim.types.dictionary}`, else it will be
|
|vim.empty_dict()|, else it will be encoded as an array.
|
||||||
encoded as an array.
|
|
||||||
|
|
||||||
• handlers: Map of language server method names to
|
• handlers: Map of language server method names to
|
||||||
|lsp-handler|
|
|lsp-handler|
|
||||||
|
@ -1027,8 +1027,7 @@ end
|
|||||||
--- \|vim.lsp.protocol.make_client_capabilities()|, passed to the language
|
--- \|vim.lsp.protocol.make_client_capabilities()|, passed to the language
|
||||||
--- server on initialization. Hint: use make_client_capabilities() and modify
|
--- server on initialization. Hint: use make_client_capabilities() and modify
|
||||||
--- its result.
|
--- its result.
|
||||||
--- - Note: To send an empty dictionary use
|
--- - Note: To send an empty dictionary use |vim.empty_dict()|, else it will be encoded as an
|
||||||
--- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an
|
|
||||||
--- array.
|
--- array.
|
||||||
---
|
---
|
||||||
--- - handlers: Map of language server method names to |lsp-handler|
|
--- - handlers: Map of language server method names to |lsp-handler|
|
||||||
|
@ -121,12 +121,15 @@ M._poll_exclude_pattern = parse('**/.git/{objects,subtree-cache}/**')
|
|||||||
function M.register(reg, ctx)
|
function M.register(reg, ctx)
|
||||||
local client_id = ctx.client_id
|
local client_id = ctx.client_id
|
||||||
local client = vim.lsp.get_client_by_id(client_id)
|
local client = vim.lsp.get_client_by_id(client_id)
|
||||||
if
|
|
||||||
-- Ill-behaved servers may not honor the client capability and try to register
|
-- Ill-behaved servers may not honor the client capability and try to register
|
||||||
-- anyway, so ignore requests when the user has opted out of the feature.
|
-- anyway, so ignore requests when the user has opted out of the feature.
|
||||||
not client.config.capabilities.workspace.didChangeWatchedFiles.dynamicRegistration
|
local has_capability = vim.tbl_get(
|
||||||
or not client.workspace_folders
|
client.config.capabilities or {},
|
||||||
then
|
'workspace',
|
||||||
|
'didChangeWatchedFiles',
|
||||||
|
'dynamicRegistration'
|
||||||
|
)
|
||||||
|
if not has_capability or not client.workspace_folders then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local watch_regs = {} --- @type table<string,{pattern:userdata,kind:integer}>
|
local watch_regs = {} --- @type table<string,{pattern:userdata,kind:integer}>
|
||||||
|
@ -4402,27 +4402,26 @@ describe('LSP', function()
|
|||||||
|
|
||||||
it("ignores registrations by servers when the client doesn't advertise support", function()
|
it("ignores registrations by servers when the client doesn't advertise support", function()
|
||||||
exec_lua(create_server_definition)
|
exec_lua(create_server_definition)
|
||||||
local result = exec_lua([[
|
exec_lua([[
|
||||||
local server = _create_server()
|
server = _create_server()
|
||||||
local client_id = vim.lsp.start({
|
|
||||||
name = 'watchfiles-test',
|
|
||||||
cmd = server.cmd,
|
|
||||||
root_dir = 'some_dir',
|
|
||||||
capabilities = {
|
|
||||||
workspace = {
|
|
||||||
didChangeWatchedFiles = {
|
|
||||||
dynamicRegistration = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
local watching = false
|
|
||||||
require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
|
require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
|
||||||
-- Since the registration is ignored, this should not execute and `watching` should stay false
|
-- Since the registration is ignored, this should not execute and `watching` should stay false
|
||||||
watching = true
|
watching = true
|
||||||
return function() end
|
return function() end
|
||||||
end
|
end
|
||||||
|
]])
|
||||||
|
|
||||||
|
local function check_registered(capabilities)
|
||||||
|
return exec_lua([[
|
||||||
|
watching = false
|
||||||
|
local client_id = vim.lsp.start({
|
||||||
|
name = 'watchfiles-test',
|
||||||
|
cmd = server.cmd,
|
||||||
|
root_dir = 'some_dir',
|
||||||
|
capabilities = ...,
|
||||||
|
}, {
|
||||||
|
reuse_client = function() return false end,
|
||||||
|
})
|
||||||
|
|
||||||
vim.lsp.handlers['client/registerCapability'](nil, {
|
vim.lsp.handlers['client/registerCapability'](nil, {
|
||||||
registrations = {
|
registrations = {
|
||||||
@ -4450,10 +4449,32 @@ describe('LSP', function()
|
|||||||
},
|
},
|
||||||
}, { client_id = client_id })
|
}, { client_id = client_id })
|
||||||
|
|
||||||
|
vim.lsp.stop_client(client_id, true)
|
||||||
return watching
|
return watching
|
||||||
]])
|
]], capabilities)
|
||||||
|
end
|
||||||
|
|
||||||
eq(false, result)
|
eq(true, check_registered(nil)) -- start{_client}() defaults to make_client_capabilities().
|
||||||
|
eq(false, check_registered(vim.empty_dict()))
|
||||||
|
eq(false, check_registered({
|
||||||
|
workspace = {
|
||||||
|
ignoreMe = true,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
eq(false, check_registered({
|
||||||
|
workspace = {
|
||||||
|
didChangeWatchedFiles = {
|
||||||
|
dynamicRegistration = false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
eq(true, check_registered({
|
||||||
|
workspace = {
|
||||||
|
didChangeWatchedFiles = {
|
||||||
|
dynamicRegistration = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
Reference in New Issue
Block a user