aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorMathias Fussenegger <f.mathias@zignar.net>2023-02-25 11:17:28 +0100
committerMathias Fussenegger <f.mathias@zignar.net>2023-02-25 11:17:28 +0100
commitf0f27e9aef7c237dd55fbb5c2cd47c2f42d01742 (patch)
tree378d8accb3f5a49d41e54bd7529a6cfdeef6bb8e /runtime/lua/vim
parent5732aa706c639b3d775573d91d1139f24624629c (diff)
downloadrneovim-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.lua1
-rw-r--r--runtime/lua/vim/_watch.lua174
-rw-r--r--runtime/lua/vim/lsp/_watchfiles.lua274
-rw-r--r--runtime/lua/vim/lsp/handlers.lua38
-rw-r--r--runtime/lua/vim/lsp/protocol.lua9
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 = {