diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index ddfb5969c6..466a31d542 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -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 +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 diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index cd374413b9..e6e7b20321 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -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