mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	 be74807eef
			
		
	
	be74807eef
	
	
	
		
			
			* docs(lua): teach lua2dox how to table * docs(lua): teach gen_vimdoc.py about local functions No more need to mark local functions with @private * docs(lua): mention @nodoc and @meta in dev-lua-doc * fixup! Co-authored-by: Justin M. Keyes <justinkz@gmail.com> --------- Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
		
			
				
	
	
		
			248 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			248 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| local bit = require('bit')
 | |
| local watch = require('vim._watch')
 | |
| local protocol = require('vim.lsp.protocol')
 | |
| local lpeg = vim.lpeg
 | |
| 
 | |
| local M = {}
 | |
| 
 | |
| --- Parses the raw pattern into an |lpeg| pattern. LPeg patterns natively support the "this" or "that"
 | |
| --- alternative constructions described in the LSP spec that cannot be expressed in a standard Lua pattern.
 | |
| ---
 | |
| ---@param pattern string The raw glob pattern
 | |
| ---@return userdata An |lpeg| representation of the pattern, or nil if the pattern is invalid.
 | |
| local function parse(pattern)
 | |
|   local l = lpeg
 | |
| 
 | |
|   local P, S, V = lpeg.P, lpeg.S, lpeg.V
 | |
|   local C, Cc, Ct, Cf = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf
 | |
| 
 | |
|   local pathsep = '/'
 | |
| 
 | |
|   local function class(inv, ranges)
 | |
|     for i, r in ipairs(ranges) do
 | |
|       ranges[i] = r[1] .. r[2]
 | |
|     end
 | |
|     local patt = l.R(unpack(ranges))
 | |
|     if inv == '!' then
 | |
|       patt = P(1) - patt
 | |
|     end
 | |
|     return patt
 | |
|   end
 | |
| 
 | |
|   local function add(acc, a)
 | |
|     return acc + a
 | |
|   end
 | |
| 
 | |
|   local function mul(acc, m)
 | |
|     return acc * m
 | |
|   end
 | |
| 
 | |
|   local function star(stars, after)
 | |
|     return (-after * (l.P(1) - pathsep)) ^ #stars * after
 | |
|   end
 | |
| 
 | |
|   local function dstar(after)
 | |
|     return (-after * l.P(1)) ^ 0 * after
 | |
|   end
 | |
| 
 | |
|   local p = P({
 | |
|     'Pattern',
 | |
|     Pattern = V('Elem') ^ -1 * V('End'),
 | |
|     Elem = Cf(
 | |
|       (V('DStar') + V('Star') + V('Ques') + V('Class') + V('CondList') + V('Literal'))
 | |
|         * (V('Elem') + V('End')),
 | |
|       mul
 | |
|     ),
 | |
|     DStar = P('**') * (P(pathsep) * (V('Elem') + V('End')) + V('End')) / dstar,
 | |
|     Star = C(P('*') ^ 1) * (V('Elem') + V('End')) / star,
 | |
|     Ques = P('?') * Cc(l.P(1) - pathsep),
 | |
|     Class = P('[') * C(P('!') ^ -1) * Ct(Ct(C(1) * '-' * C(P(1) - ']')) ^ 1 * ']') / class,
 | |
|     CondList = P('{') * Cf(V('Cond') * (P(',') * V('Cond')) ^ 0, add) * '}',
 | |
|     -- TODO: '*' inside a {} condition is interpreted literally but should probably have the same
 | |
|     -- wildcard semantics it usually has.
 | |
|     -- Fixing this is non-trivial because '*' should match non-greedily up to "the rest of the
 | |
|     -- pattern" which in all other cases is the entire succeeding part of the pattern, but at the end of a {}
 | |
|     -- condition means "everything after the {}" where several other options separated by ',' may
 | |
|     -- exist in between that should not be matched by '*'.
 | |
|     Cond = Cf((V('Ques') + V('Class') + V('CondList') + (V('Literal') - S(',}'))) ^ 1, mul)
 | |
|       + Cc(l.P(0)),
 | |
|     Literal = P(1) / l.P,
 | |
|     End = P(-1) * Cc(l.P(-1)),
 | |
|   })
 | |
| 
 | |
|   return p:match(pattern)
 | |
| end
 | |
| 
 | |
| ---@private
 | |
| --- Implementation of LSP 3.17.0's pattern matching: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern
 | |
| ---
 | |
| ---@param pattern string|table The glob pattern (raw or parsed) to match.
 | |
| ---@param s string The string to match against pattern.
 | |
| ---@return boolean Whether or not pattern matches s.
 | |
| function M._match(pattern, s)
 | |
|   if type(pattern) == 'string' then
 | |
|     pattern = parse(pattern)
 | |
|   end
 | |
|   return pattern:match(s) ~= nil
 | |
| end
 | |
| 
 | |
| M._watchfunc = (vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1) and watch.watch or watch.poll
 | |
| 
 | |
| ---@type table<number, table<number, function[]>> client id -> registration id -> cancel function
 | |
| local cancels = vim.defaulttable()
 | |
| 
 | |
| local queue_timeout_ms = 100
 | |
| ---@type table<number, uv.uv_timer_t> client id -> libuv timer which will send queued changes at its timeout
 | |
| local queue_timers = {}
 | |
| ---@type table<number, lsp.FileEvent[]> client id -> set of queued changes to send in a single LSP notification
 | |
| local change_queues = {}
 | |
| ---@type table<number, 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()
 | |
| 
 | |
| 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 Lpeg pattern
 | |
| M._poll_exclude_pattern = parse('**/.git/{objects,subtree-cache}/**')
 | |
|   + parse('**/node_modules/*/**')
 | |
|   + parse('**/.hg/store/**')
 | |
| 
 | |
| --- Registers the workspace/didChangeWatchedFiles capability dynamically.
 | |
| ---
 | |
| ---@param reg table LSP Registration object.
 | |
| ---@param ctx table Context from the |lsp-handler|.
 | |
| function M.register(reg, ctx)
 | |
|   local client_id = ctx.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
 | |
|     -- anyway, so ignore requests when the user has opted out of the feature.
 | |
|     not client.config.capabilities.workspace.didChangeWatchedFiles.dynamicRegistration
 | |
|     or not client.workspace_folders
 | |
|   then
 | |
|     return
 | |
|   end
 | |
|   local watch_regs = {} --- @type table<string,{pattern:userdata,kind:integer}>
 | |
|   for _, w in ipairs(reg.registerOptions.watchers) do
 | |
|     local relative_pattern = false
 | |
|     local glob_patterns = {} --- @type {baseUri:string, pattern: string}[]
 | |
|     if type(w.globPattern) == 'string' then
 | |
|       for _, folder in ipairs(client.workspace_folders) do
 | |
|         table.insert(glob_patterns, { baseUri = folder.uri, pattern = w.globPattern })
 | |
|       end
 | |
|     else
 | |
|       relative_pattern = true
 | |
|       table.insert(glob_patterns, w.globPattern)
 | |
|     end
 | |
|     for _, glob_pattern in ipairs(glob_patterns) do
 | |
|       local base_dir = nil ---@type string?
 | |
|       if type(glob_pattern.baseUri) == 'string' then
 | |
|         base_dir = glob_pattern.baseUri
 | |
|       elseif type(glob_pattern.baseUri) == 'table' then
 | |
|         base_dir = glob_pattern.baseUri.uri
 | |
|       end
 | |
|       assert(base_dir, "couldn't identify root of watch")
 | |
|       base_dir = vim.uri_to_fname(base_dir)
 | |
| 
 | |
|       ---@type integer
 | |
|       local kind = w.kind
 | |
|         or protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete
 | |
| 
 | |
|       local pattern = parse(glob_pattern.pattern)
 | |
|       assert(pattern, 'invalid pattern: ' .. glob_pattern.pattern)
 | |
|       if relative_pattern then
 | |
|         pattern = lpeg.P(base_dir .. '/') * pattern
 | |
|       end
 | |
| 
 | |
|       watch_regs[base_dir] = watch_regs[base_dir] or {}
 | |
|       table.insert(watch_regs[base_dir], {
 | |
|         pattern = pattern,
 | |
|         kind = kind,
 | |
|       })
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   local callback = function(base_dir)
 | |
|     return function(fullpath, change_type)
 | |
|       for _, w in ipairs(watch_regs[base_dir]) do
 | |
|         change_type = to_lsp_change_type[change_type]
 | |
|         -- e.g. match kind with Delete bit (0b0100) to Delete change_type (3)
 | |
|         local kind_mask = bit.lshift(1, change_type - 1)
 | |
|         local change_type_match = bit.band(w.kind, kind_mask) == kind_mask
 | |
|         if M._match(w.pattern, fullpath) and change_type_match then
 | |
|           local change = {
 | |
|             uri = vim.uri_from_fname(fullpath),
 | |
|             type = 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()
 | |
|               client.notify('workspace/didChangeWatchedFiles', {
 | |
|                 changes = change_queues[client_id],
 | |
|               })
 | |
|               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 table LSP Unregistration object.
 | |
| ---@param ctx table Context from the |lsp-handler|.
 | |
| function M.unregister(unreg, ctx)
 | |
|   local client_id = ctx.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
 | |
| 
 | |
| return M
 |