_watchfiles.lua (6948B)
1 local bit = require('bit') 2 local glob = vim.glob 3 local watch = vim._watch 4 local protocol = require('vim.lsp.protocol') 5 local lpeg = vim.lpeg 6 7 local M = {} 8 9 if vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1 then 10 M._watchfunc = watch.watch 11 elseif vim.fn.executable('inotifywait') == 1 then 12 M._watchfunc = watch.inotify 13 else 14 M._watchfunc = watch.watchdirs 15 end 16 17 ---@type table<integer, table<string, function[]>> client id -> registration id -> cancel function 18 local cancels = vim.defaulttable() 19 20 local queue_timeout_ms = 100 21 ---@type table<integer, uv.uv_timer_t> client id -> libuv timer which will send queued changes at its timeout 22 local queue_timers = {} 23 ---@type table<integer, lsp.FileEvent[]> client id -> set of queued changes to send in a single LSP notification 24 local change_queues = {} 25 ---@type table<integer, table<string, lsp.FileChangeType>> client id -> URI -> last type of change processed 26 --- Used to prune consecutive events of the same type for the same file 27 local change_cache = vim.defaulttable() 28 29 ---@type table<vim._watch.FileChangeType, lsp.FileChangeType> 30 local to_lsp_change_type = { 31 [watch.FileChangeType.Created] = protocol.FileChangeType.Created, 32 [watch.FileChangeType.Changed] = protocol.FileChangeType.Changed, 33 [watch.FileChangeType.Deleted] = protocol.FileChangeType.Deleted, 34 } 35 36 --- Default excludes the same as VSCode's `files.watcherExclude` setting. 37 --- https://github.com/microsoft/vscode/blob/eef30e7165e19b33daa1e15e92fa34ff4a5df0d3/src/vs/workbench/contrib/files/browser/files.contribution.ts#L261 38 ---@type vim.lpeg.Pattern parsed Lpeg pattern 39 M._poll_exclude_pattern = glob.to_lpeg('**/.git/{objects,subtree-cache}/**') 40 + glob.to_lpeg('**/node_modules/*/**') 41 + glob.to_lpeg('**/.hg/store/**') 42 43 --- Registers the workspace/didChangeWatchedFiles capability dynamically. 44 --- 45 ---@param reg lsp.Registration LSP Registration object. 46 ---@param client_id integer Client ID. 47 function M.register(reg, client_id) 48 local client = assert(vim.lsp.get_client_by_id(client_id), 'Client must be running') 49 -- Ill-behaved servers may not honor the client capability and try to register 50 -- anyway, so ignore requests when the user has opted out of the feature. 51 local has_capability = 52 vim.tbl_get(client.capabilities, 'workspace', 'didChangeWatchedFiles', 'dynamicRegistration') 53 if not has_capability or not client.workspace_folders then 54 return 55 end 56 local register_options = reg.registerOptions --[[@as lsp.DidChangeWatchedFilesRegistrationOptions]] 57 ---@type table<string, {pattern: vim.lpeg.Pattern, kind: lsp.WatchKind}[]> by base_dir 58 local watch_regs = vim.defaulttable() 59 for _, w in ipairs(register_options.watchers) do 60 local kind = w.kind 61 or (protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete) 62 local glob_pattern = w.globPattern 63 64 if type(glob_pattern) == 'string' then 65 local pattern = glob.to_lpeg(glob_pattern) 66 if not pattern then 67 error('Cannot parse pattern: ' .. glob_pattern) 68 end 69 for _, folder in ipairs(client.workspace_folders) do 70 local base_dir = vim.uri_to_fname(folder.uri) 71 table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind }) 72 end 73 else 74 local base_uri = glob_pattern.baseUri 75 local uri = type(base_uri) == 'string' and base_uri or base_uri.uri 76 local base_dir = vim.uri_to_fname(uri) 77 local pattern = glob.to_lpeg(glob_pattern.pattern) 78 if not pattern then 79 error('Cannot parse pattern: ' .. glob_pattern.pattern) 80 end 81 pattern = lpeg.P(base_dir .. '/') * pattern 82 table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind }) 83 end 84 end 85 86 ---@param base_dir string 87 local callback = function(base_dir) 88 return function(fullpath, change_type) 89 local registrations = watch_regs[base_dir] 90 for _, w in ipairs(registrations) do 91 local lsp_change_type = assert( 92 to_lsp_change_type[change_type], 93 'Must receive change type Created, Changed or Deleted' 94 ) 95 -- e.g. match kind with Delete bit (0b0100) to Delete change_type (3) 96 local kind_mask = bit.lshift(1, lsp_change_type - 1) 97 local change_type_match = bit.band(w.kind, kind_mask) == kind_mask 98 if w.pattern:match(fullpath) ~= nil and change_type_match then 99 ---@type lsp.FileEvent 100 local change = { 101 uri = vim.uri_from_fname(fullpath), 102 type = lsp_change_type, 103 } 104 105 local last_type = change_cache[client_id][change.uri] 106 if last_type ~= change.type then 107 change_queues[client_id] = change_queues[client_id] or {} 108 table.insert(change_queues[client_id], change) 109 change_cache[client_id][change.uri] = change.type 110 end 111 112 if not queue_timers[client_id] then 113 queue_timers[client_id] = vim.defer_fn(function() 114 ---@type lsp.DidChangeWatchedFilesParams 115 local params = { 116 changes = change_queues[client_id], 117 } 118 client:notify('workspace/didChangeWatchedFiles', params) 119 queue_timers[client_id] = nil 120 change_queues[client_id] = nil 121 change_cache[client_id] = nil 122 end, queue_timeout_ms) 123 end 124 125 break -- if an event matches multiple watchers, only send one notification 126 end 127 end 128 end 129 end 130 131 for base_dir, watches in pairs(watch_regs) do 132 local include_pattern = vim.iter(watches):fold(lpeg.P(false), function(acc, w) 133 return acc + w.pattern 134 end) 135 136 table.insert( 137 cancels[client_id][reg.id], 138 M._watchfunc(base_dir, { 139 uvflags = { 140 recursive = true, 141 }, 142 -- include_pattern will ensure the pattern from *any* watcher definition for the 143 -- base_dir matches. This first pass prevents polling for changes to files that 144 -- will never be sent to the LSP server. A second pass in the callback is still necessary to 145 -- match a *particular* pattern+kind pair. 146 include_pattern = include_pattern, 147 exclude_pattern = M._poll_exclude_pattern, 148 }, callback(base_dir)) 149 ) 150 end 151 end 152 153 --- Unregisters the workspace/didChangeWatchedFiles capability dynamically. 154 --- 155 ---@param unreg lsp.Unregistration LSP Unregistration object. 156 ---@param client_id integer Client ID. 157 function M.unregister(unreg, client_id) 158 local client_cancels = cancels[client_id] 159 local reg_cancels = client_cancels[unreg.id] 160 while #reg_cancels > 0 do 161 table.remove(reg_cancels)() 162 end 163 client_cancels[unreg.id] = nil 164 if not next(cancels[client_id]) then 165 cancels[client_id] = nil 166 end 167 end 168 169 --- @param client_id integer 170 function M.cancel(client_id) 171 for _, reg_cancels in pairs(cancels[client_id]) do 172 for _, cancel in pairs(reg_cancels) do 173 cancel() 174 end 175 end 176 cancels[client_id] = nil 177 end 178 179 return M