fix(lsp): skip invalid file watcher globs #40376 #40396

Some servers register `workspace/didChangeWatchedFiles` watchers for URI
schemes that cannot be watched locally. Skipping the unsupported glob
and keep the rest of the registration batch active.
This commit is contained in:
Barrett Ruth
2026-06-25 02:44:33 -05:00
committed by GitHub
parent 3c924d13fe
commit 4d9e5acfb5
2 changed files with 81 additions and 16 deletions

View File

@@ -1,11 +1,53 @@
local bit = require('bit')
local glob = vim.glob
local watch = vim._watch
local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
local lpeg = vim.lpeg
local M = {}
-- Use a bounded set to suppress repeat warnings without unbounded growth.
---@type table<string, true>
local logged_invalid_patterns = {}
---@type string[]
local logged_invalid_pattern_keys = {}
local logged_invalid_pattern_key_index = 1
---@param key string
local function remember_invalid_pattern(key)
local old_key = logged_invalid_pattern_keys[logged_invalid_pattern_key_index]
if old_key then
logged_invalid_patterns[old_key] = nil
end
logged_invalid_patterns[key] = true
logged_invalid_pattern_keys[logged_invalid_pattern_key_index] = key
logged_invalid_pattern_key_index = logged_invalid_pattern_key_index % 20 + 1
end
--- Parses a glob pattern and logs invalid patterns once.
---@param pattern string
---@param client vim.lsp.Client
---@return vim.lpeg.Pattern?
local function glob_to_lpeg(pattern, client)
local ok, pattern_or_err = pcall(glob.to_lpeg, pattern)
if ok then
return pattern_or_err
end
local key = string.format('%d:%s', client.id, pattern)
if not logged_invalid_patterns[key] then
log.error(
string.format(
'LSP[%s] skipping invalid workspace/didChangeWatchedFiles globPattern',
client.name
),
pattern,
pattern_or_err
)
remember_invalid_pattern(key)
end
end
if vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1 then
M._watchfunc = watch.watch
elseif vim.fn.executable('inotifywait') == 1 then
@@ -62,24 +104,22 @@ function M.register(reg, client_id)
local glob_pattern = w.globPattern
if type(glob_pattern) == 'string' then
local pattern = glob.to_lpeg(glob_pattern)
if not pattern then
error('Cannot parse pattern: ' .. glob_pattern)
end
for _, folder in ipairs(client.workspace_folders) do
local base_dir = vim.uri_to_fname(folder.uri)
table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind })
local pattern = glob_to_lpeg(glob_pattern, client)
if pattern then
for _, folder in ipairs(client.workspace_folders) do
local base_dir = vim.uri_to_fname(folder.uri)
table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind })
end
end
else
local base_uri = glob_pattern.baseUri
local uri = type(base_uri) == 'string' and base_uri or base_uri.uri
local base_dir = vim.uri_to_fname(uri)
local pattern = glob.to_lpeg(glob_pattern.pattern)
if not pattern then
error('Cannot parse pattern: ' .. glob_pattern.pattern)
local pattern = glob_to_lpeg(glob_pattern.pattern, client)
if pattern then
local relative_pattern = lpeg.P(base_dir .. '/') * pattern --[[@as vim.lpeg.Pattern]]
table.insert(watch_regs[base_dir], { pattern = relative_pattern, kind = kind })
end
pattern = lpeg.P(base_dir .. '/') * pattern
table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind })
end
end

View File

@@ -3730,6 +3730,8 @@ describe('LSP', function()
exec_lua(create_server_definition)
local result = exec_lua(function()
local logfile = vim.lsp.log.get_filename()
vim.fn.writefile({ '' }, logfile)
local server = _G._create_server()
local client_id = assert(vim.lsp.start({
name = 'watchfiles-test',
@@ -3770,6 +3772,17 @@ describe('LSP', function()
method = 'workspace/didChangeWatchedFiles',
registerOptions = {
watchers = {
{
globPattern = 'a/**b',
kind = 7,
},
{
globPattern = {
baseUri = vim.uri_from_fname(root_dir),
pattern = '{foo}',
},
kind = 7,
},
{
globPattern = '**/watch',
kind = 7,
@@ -3797,12 +3810,13 @@ describe('LSP', function()
vim.lsp.get_client_by_id(client_id):stop()
return server.messages
return { logfile = logfile, messages = server.messages }
end)
local uri = vim.uri_from_fname(root_dir .. '/watch')
local messages = result.messages
eq(6, #result)
eq(6, #messages)
eq({
method = 'workspace/didChangeWatchedFiles',
@@ -3814,7 +3828,7 @@ describe('LSP', function()
},
},
},
}, result[3])
}, messages[3])
eq({
method = 'workspace/didChangeWatchedFiles',
@@ -3826,7 +3840,18 @@ describe('LSP', function()
},
},
},
}, result[4])
}, messages[4])
t.assert_log(
'%[ERROR%].-skipping invalid workspace/didChangeWatchedFiles globPattern.-'
.. pesc('a/**b'),
result.logfile
)
t.assert_log(
'%[ERROR%].-skipping invalid workspace/didChangeWatchedFiles globPattern.-'
.. pesc('{foo}'),
result.logfile
)
end
)
end