diff options
author | Mathias Fussenegger <f.mathias@zignar.net> | 2023-02-25 11:17:28 +0100 |
---|---|---|
committer | Mathias Fussenegger <f.mathias@zignar.net> | 2023-02-25 11:17:28 +0100 |
commit | f0f27e9aef7c237dd55fbb5c2cd47c2f42d01742 (patch) | |
tree | 378d8accb3f5a49d41e54bd7529a6cfdeef6bb8e /runtime/lua/vim | |
parent | 5732aa706c639b3d775573d91d1139f24624629c (diff) | |
download | rneovim-f0f27e9aef7c237dd55fbb5c2cd47c2f42d01742.tar.gz rneovim-f0f27e9aef7c237dd55fbb5c2cd47c2f42d01742.tar.bz2 rneovim-f0f27e9aef7c237dd55fbb5c2cd47c2f42d01742.zip |
Revert "feat(lsp): implement workspace/didChangeWatchedFiles (#21293)"
This reverts commit 5732aa706c639b3d775573d91d1139f24624629c.
Causes editor to freeze in projects with many watcher registrations
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/_editor.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/_watch.lua | 174 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_watchfiles.lua | 274 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 38 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 9 |
5 files changed, 9 insertions, 487 deletions
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 92444ff550..3f27e61810 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -37,7 +37,6 @@ for k, v in pairs({ health = true, fs = true, secure = true, - _watch = true, }) do vim._submodules[k] = v end diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua deleted file mode 100644 index dba1522ec8..0000000000 --- a/runtime/lua/vim/_watch.lua +++ /dev/null @@ -1,174 +0,0 @@ -local M = {} - ---- Enumeration describing the types of events watchers will emit. -M.FileChangeType = vim.tbl_add_reverse_lookup({ - Created = 1, - Changed = 2, - Deleted = 3, -}) - ----@private ---- Joins filepath elements by static '/' separator ---- ----@param ... (string) The path elements. -local function filepath_join(...) - return table.concat({ ... }, '/') -end - ----@private ---- Stops and closes a libuv |uv_fs_event_t| or |uv_fs_poll_t| handle ---- ----@param handle (uv_fs_event_t|uv_fs_poll_t) The handle to stop -local function stop(handle) - local _, stop_err = handle:stop() - assert(not stop_err, stop_err) - local is_closing, close_err = handle:is_closing() - assert(not close_err, close_err) - if not is_closing then - handle:close() - end -end - ---- Initializes and starts a |uv_fs_event_t| ---- ----@param path (string) The path to watch ----@param opts (table|nil) Additional options ---- - uvflags (table|nil) ---- Same flags as accepted by |uv.fs_event_start()| ----@param callback (function) The function called when new events ----@returns (function) A function to stop the watch -function M.watch(path, opts, callback) - vim.validate({ - path = { path, 'string', false }, - opts = { opts, 'table', true }, - callback = { callback, 'function', false }, - }) - - path = vim.fs.normalize(path) - local uvflags = opts and opts.uvflags or {} - local handle, new_err = vim.loop.new_fs_event() - assert(not new_err, new_err) - local _, start_err = handle:start(path, uvflags, function(err, filename, events) - assert(not err, err) - local fullpath = path - if filename then - filename = filename:gsub('\\', '/') - fullpath = filepath_join(fullpath, filename) - end - local change_type = events.change and M.FileChangeType.Changed or 0 - if events.rename then - local _, staterr, staterrname = vim.loop.fs_stat(fullpath) - if staterrname == 'ENOENT' then - change_type = M.FileChangeType.Deleted - else - assert(not staterr, staterr) - change_type = M.FileChangeType.Created - end - end - callback(fullpath, change_type) - end) - assert(not start_err, start_err) - return function() - stop(handle) - end -end - -local default_poll_interval_ms = 2000 - ----@private ---- Implementation for poll, hiding internally-used parameters. ---- ----@param watches (table|nil) A tree structure to maintain state for recursive watches. ---- - handle (uv_fs_poll_t) ---- The libuv handle ---- - cancel (function) ---- A function that cancels the handle and all children's handles ---- - is_dir (boolean) ---- Indicates whether the path is a directory (and the poll should ---- be invoked recursively) ---- - children (table|nil) ---- A mapping of directory entry name to its recursive watches -local function poll_internal(path, opts, callback, watches) - path = vim.fs.normalize(path) - local interval = opts and opts.interval or default_poll_interval_ms - watches = watches or { - is_dir = true, - } - - if not watches.handle then - local poll, new_err = vim.loop.new_fs_poll() - assert(not new_err, new_err) - watches.handle = poll - local _, start_err = poll:start( - path, - interval, - vim.schedule_wrap(function(err) - if err == 'ENOENT' then - return - end - assert(not err, err) - poll_internal(path, opts, callback, watches) - callback(path, M.FileChangeType.Changed) - end) - ) - assert(not start_err, start_err) - callback(path, M.FileChangeType.Created) - end - - watches.cancel = function() - if watches.children then - for _, w in pairs(watches.children) do - w.cancel() - end - end - stop(watches.handle) - end - - if watches.is_dir then - watches.children = watches.children or {} - local exists = {} - for name, ftype in vim.fs.dir(path) do - exists[name] = true - if not watches.children[name] then - watches.children[name] = { - is_dir = ftype == 'directory', - } - poll_internal(filepath_join(path, name), opts, callback, watches.children[name]) - end - end - - local newchildren = {} - for name, watch in pairs(watches.children) do - if exists[name] then - newchildren[name] = watch - else - watch.cancel() - watches.children[name] = nil - callback(path .. '/' .. name, M.FileChangeType.Deleted) - end - end - watches.children = newchildren - end - - return watches.cancel -end - ---- Initializes and starts a |uv_fs_poll_t| recursively watching every file underneath the ---- directory at path. ---- ----@param path (string) The path to watch. Must refer to a directory. ----@param opts (table|nil) Additional options ---- - interval (number|nil) ---- Polling interval in ms as passed to |uv.fs_poll_start()|. Defaults to 2000. ----@param callback (function) The function called when new events ----@returns (function) A function to stop the watch. -function M.poll(path, opts, callback) - vim.validate({ - path = { path, 'string', false }, - opts = { opts, 'table', true }, - callback = { callback, 'function', false }, - }) - return poll_internal(path, opts, callback, nil) -end - -return M diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua deleted file mode 100644 index b9268b963c..0000000000 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ /dev/null @@ -1,274 +0,0 @@ -local bit = require('bit') -local watch = require('vim._watch') -local protocol = require('vim.lsp.protocol') - -local M = {} - ----@private ----Parses the raw pattern into a number of Lua-native patterns. ---- ----@param pattern string The raw glob pattern ----@return table A list of Lua patterns. A match with any of them matches the input glob pattern. -local function parse(pattern) - local patterns = { '' } - - local path_sep = '[/\\]' - local non_path_sep = '[^/\\]' - - local function append(chunks) - local new_patterns = {} - for _, p in ipairs(patterns) do - for _, chunk in ipairs(chunks) do - table.insert(new_patterns, p .. chunk) - end - end - patterns = new_patterns - end - - local function split(s, sep) - local segments = {} - local segment = '' - local in_braces = false - local in_brackets = false - for i = 1, #s do - local c = string.sub(s, i, i) - if c == sep and not in_braces and not in_brackets then - table.insert(segments, segment) - segment = '' - else - if c == '{' then - in_braces = true - elseif c == '}' then - in_braces = false - elseif c == '[' then - in_brackets = true - elseif c == ']' then - in_brackets = false - end - segment = segment .. c - end - end - if segment ~= '' then - table.insert(segments, segment) - end - return segments - end - - local function escape(c) - if - c == '?' - or c == '.' - or c == '(' - or c == ')' - or c == '%' - or c == '[' - or c == ']' - or c == '*' - or c == '+' - or c == '-' - then - return '%' .. c - end - return c - end - - local segments = split(pattern, '/') - for i, segment in ipairs(segments) do - local last_seg = i == #segments - if segment == '**' then - local chunks = { - path_sep .. '-', - '.-' .. path_sep, - } - if last_seg then - chunks = { '.-' } - end - append(chunks) - else - local in_braces = false - local brace_val = '' - local in_brackets = false - local bracket_val = '' - for j = 1, #segment do - local char = string.sub(segment, j, j) - if char ~= '}' and in_braces then - brace_val = brace_val .. char - else - if in_brackets and (char ~= ']' or bracket_val == '') then - local res - if char == '-' then - res = char - elseif bracket_val == '' and char == '!' then - res = '^' - elseif char == '/' then - res = '' - else - res = escape(char) - end - bracket_val = bracket_val .. res - else - if char == '{' then - in_braces = true - elseif char == '[' then - in_brackets = true - elseif char == '}' then - local choices = split(brace_val, ',') - local parsed_choices = {} - for _, choice in ipairs(choices) do - table.insert(parsed_choices, parse(choice)) - end - append(vim.tbl_flatten(parsed_choices)) - in_braces = false - brace_val = '' - elseif char == ']' then - append({ '[' .. bracket_val .. ']' }) - in_brackets = false - bracket_val = '' - elseif char == '?' then - append({ non_path_sep }) - elseif char == '*' then - append({ non_path_sep .. '-' }) - else - append({ escape(char) }) - end - end - end - end - - if not last_seg and (segments[i + 1] ~= '**' or i + 1 < #segments) then - append({ path_sep }) - end - end - end - - return patterns -end - ----@private ---- Implementation of LSP 3.17.0's pattern matching: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#pattern ---- Modeled after VSCode's implementation: https://github.com/microsoft/vscode/blob/0319eed971719ad48e9093daba9d65a5013ec5ab/src/vs/base/common/glob.ts#L509 ---- ----@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 - -- Since Lua's built-in string pattern matching does not have an alternate - -- operator like '|', `parse` will construct one pattern for each possible - -- alternative. Any pattern that matches thus matches the glob. - for _, p in ipairs(pattern) do - if s:match('^' .. p .. '$') then - return true - end - end - return false -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_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, -} - ---- 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) - for _, w in ipairs(reg.registerOptions.watchers) do - local glob_patterns = {} - 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 - table.insert(glob_patterns, w.globPattern) - end - for _, glob_pattern in ipairs(glob_patterns) do - local pattern = parse(glob_pattern.pattern) - local base_dir = nil - 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) - local kind = w.kind - or protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete - - table.insert( - cancels[client_id][reg.id], - M._watchfunc(base_dir, { uvflags = { recursive = true } }, function(fullpath, change_type) - 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(kind, kind_mask) == kind_mask - if not M._match(pattern, fullpath) or not change_type_match then - return - end - - 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 - end) - ) - end - 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 diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index ee5b63d260..5096100a60 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -117,35 +117,15 @@ M['window/showMessageRequest'] = function(_, result) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability -M['client/registerCapability'] = function(_, result, ctx) - local log_unsupported = false - for _, reg in ipairs(result.registrations) do - if reg.method == 'workspace/didChangeWatchedFiles' then - require('vim.lsp._watchfiles').register(reg, ctx) - else - log_unsupported = true - end - end - if log_unsupported then - local client_id = ctx.client_id - local warning_tpl = 'The language server %s triggers a registerCapability ' - .. 'handler despite dynamicRegistration set to false. ' - .. 'Report upstream, this warning is harmless' - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format('id=%d', client_id) - local warning = string.format(warning_tpl, client_name) - log.warn(warning) - end - return vim.NIL -end - ---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability -M['client/unregisterCapability'] = function(_, result, ctx) - for _, unreg in ipairs(result.unregisterations) do - if unreg.method == 'workspace/didChangeWatchedFiles' then - require('vim.lsp._watchfiles').unregister(unreg, ctx) - end - end +M['client/registerCapability'] = function(_, _, ctx) + local client_id = ctx.client_id + local warning_tpl = 'The language server %s triggers a registerCapability ' + .. 'handler despite dynamicRegistration set to false. ' + .. 'Report upstream, this warning is harmless' + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format('id=%d', client_id) + local warning = string.format(warning_tpl, client_name) + log.warn(warning) return vim.NIL end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index df1ab26667..12345b6c8c 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -28,10 +28,6 @@ end ---@class lsp.MessageActionItem ---@field title string ----@class lsp.FileEvent ----@field uri string ----@field type lsp.FileChangeType - local constants = { DiagnosticSeverity = { -- Reports an error. @@ -64,7 +60,6 @@ local constants = { }, -- The file event type. - ---@enum lsp.FileChangeType FileChangeType = { -- The file got created. Created = 1, @@ -846,10 +841,6 @@ function protocol.make_client_capabilities() semanticTokens = { refreshSupport = true, }, - didChangeWatchedFiles = { - dynamicRegistration = true, - relativePatternSupport = true, - }, }, experimental = nil, window = { |