mirror of
https://github.com/neovim/neovim.git
synced 2026-07-22 17:02:59 +00:00
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.
220 lines
8.0 KiB
Lua
220 lines
8.0 KiB
Lua
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
|
|
M._watchfunc = watch.inotify
|
|
else
|
|
M._watchfunc = watch.watchdirs
|
|
end
|
|
|
|
---@type table<integer, table<string, function[]>> client id -> registration id -> cancel function
|
|
local cancels = vim.defaulttable()
|
|
|
|
local queue_timeout_ms = 100
|
|
---@type table<integer, uv.uv_timer_t> client id -> libuv timer which will send queued changes at its timeout
|
|
local queue_timers = {}
|
|
---@type table<integer, lsp.FileEvent[]> client id -> set of queued changes to send in a single LSP notification
|
|
local change_queues = {}
|
|
---@type table<integer, table<string, lsp.FileChangeType>> client id -> URI -> last type of change processed
|
|
--- Used to prune consecutive events of the same type for the same file
|
|
local change_cache = vim.defaulttable()
|
|
|
|
---@type table<vim._watch.FileChangeType, lsp.FileChangeType>
|
|
local to_lsp_change_type = {
|
|
[watch.FileChangeType.Created] = protocol.FileChangeType.Created,
|
|
[watch.FileChangeType.Changed] = protocol.FileChangeType.Changed,
|
|
[watch.FileChangeType.Deleted] = protocol.FileChangeType.Deleted,
|
|
}
|
|
|
|
--- Default excludes the same as VSCode's `files.watcherExclude` setting.
|
|
--- https://github.com/microsoft/vscode/blob/eef30e7165e19b33daa1e15e92fa34ff4a5df0d3/src/vs/workbench/contrib/files/browser/files.contribution.ts#L261
|
|
---@type vim.lpeg.Pattern parsed Lpeg pattern
|
|
M._poll_exclude_pattern = glob.to_lpeg('**/.git/{objects,subtree-cache}/**')
|
|
+ glob.to_lpeg('**/node_modules/*/**')
|
|
+ glob.to_lpeg('**/.hg/store/**')
|
|
|
|
--- Registers the workspace/didChangeWatchedFiles capability dynamically.
|
|
---
|
|
---@param reg lsp.Registration LSP Registration object.
|
|
---@param client_id integer Client ID.
|
|
function M.register(reg, client_id)
|
|
local client = assert(vim.lsp.get_client_by_id(client_id), 'Client must be running')
|
|
-- 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.
|
|
local has_capability =
|
|
vim.tbl_get(client.capabilities, 'workspace', 'didChangeWatchedFiles', 'dynamicRegistration')
|
|
if not has_capability or not client.workspace_folders then
|
|
return
|
|
end
|
|
local register_options = reg.registerOptions --[[@as lsp.DidChangeWatchedFilesRegistrationOptions]]
|
|
---@type table<string, {pattern: vim.lpeg.Pattern, kind: lsp.WatchKind}[]> by base_dir
|
|
local watch_regs = vim.defaulttable()
|
|
for _, w in ipairs(register_options.watchers) do
|
|
local kind = w.kind
|
|
or (protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete)
|
|
local glob_pattern = w.globPattern
|
|
|
|
if type(glob_pattern) == 'string' then
|
|
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, 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
|
|
end
|
|
end
|
|
|
|
---@param base_dir string
|
|
local callback = function(base_dir)
|
|
return function(fullpath, change_type)
|
|
local registrations = watch_regs[base_dir]
|
|
for _, w in ipairs(registrations) do
|
|
local lsp_change_type = assert(
|
|
to_lsp_change_type[change_type],
|
|
'Must receive change type Created, Changed or Deleted'
|
|
)
|
|
-- e.g. match kind with Delete bit (0b0100) to Delete change_type (3)
|
|
local kind_mask = bit.lshift(1, lsp_change_type - 1)
|
|
local change_type_match = bit.band(w.kind, kind_mask) == kind_mask
|
|
if w.pattern:match(fullpath) ~= nil and change_type_match then
|
|
---@type lsp.FileEvent
|
|
local change = {
|
|
uri = vim.uri_from_fname(fullpath),
|
|
type = lsp_change_type,
|
|
}
|
|
|
|
local last_type = change_cache[client_id][change.uri]
|
|
if last_type ~= change.type then
|
|
change_queues[client_id] = change_queues[client_id] or {}
|
|
table.insert(change_queues[client_id], change)
|
|
change_cache[client_id][change.uri] = change.type
|
|
end
|
|
|
|
if not queue_timers[client_id] then
|
|
queue_timers[client_id] = vim.defer_fn(function()
|
|
---@type lsp.DidChangeWatchedFilesParams
|
|
local params = {
|
|
changes = change_queues[client_id],
|
|
}
|
|
client:notify('workspace/didChangeWatchedFiles', params)
|
|
queue_timers[client_id] = nil
|
|
change_queues[client_id] = nil
|
|
change_cache[client_id] = nil
|
|
end, queue_timeout_ms)
|
|
end
|
|
|
|
break -- if an event matches multiple watchers, only send one notification
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for base_dir, watches in pairs(watch_regs) do
|
|
local include_pattern = vim.iter(watches):fold(lpeg.P(false), function(acc, w)
|
|
return acc + w.pattern
|
|
end)
|
|
|
|
table.insert(
|
|
cancels[client_id][reg.id],
|
|
M._watchfunc(base_dir, {
|
|
uvflags = {
|
|
recursive = true,
|
|
},
|
|
-- include_pattern will ensure the pattern from *any* watcher definition for the
|
|
-- base_dir matches. This first pass prevents polling for changes to files that
|
|
-- will never be sent to the LSP server. A second pass in the callback is still necessary to
|
|
-- match a *particular* pattern+kind pair.
|
|
include_pattern = include_pattern,
|
|
exclude_pattern = M._poll_exclude_pattern,
|
|
}, callback(base_dir))
|
|
)
|
|
end
|
|
end
|
|
|
|
--- Unregisters the workspace/didChangeWatchedFiles capability dynamically.
|
|
---
|
|
---@param unreg lsp.Unregistration LSP Unregistration object.
|
|
---@param client_id integer Client ID.
|
|
function M.unregister(unreg, client_id)
|
|
local client_cancels = cancels[client_id]
|
|
local reg_cancels = client_cancels[unreg.id]
|
|
while #reg_cancels > 0 do
|
|
table.remove(reg_cancels)()
|
|
end
|
|
client_cancels[unreg.id] = nil
|
|
if not next(cancels[client_id]) then
|
|
cancels[client_id] = nil
|
|
end
|
|
end
|
|
|
|
--- @param client_id integer
|
|
function M.cancel(client_id)
|
|
for _, reg_cancels in pairs(cancels[client_id]) do
|
|
for _, cancel in pairs(reg_cancels) do
|
|
cancel()
|
|
end
|
|
end
|
|
cancels[client_id] = nil
|
|
end
|
|
|
|
return M
|