aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/news.txt3
-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
-rw-r--r--test/functional/lua/watch_spec.lua195
-rw-r--r--test/functional/plugin/lsp/watchfiles_spec.lua173
-rw-r--r--test/functional/plugin/lsp_spec.lua421
9 files changed, 9 insertions, 1279 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 53c2b44c5f..23bb6d4343 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -171,9 +171,6 @@ The following new APIs or features were added.
• |vim.treesitter.foldexpr()| can be used for 'foldexpr' to use treesitter for folding.
-• Added support for the `workspace/didChangeWatchedFiles` capability to the
- LSP client to notify servers of file changes on disk.
-
==============================================================================
CHANGED FEATURES *news-changes*
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 = {
diff --git a/test/functional/lua/watch_spec.lua b/test/functional/lua/watch_spec.lua
deleted file mode 100644
index 19bb411ce6..0000000000
--- a/test/functional/lua/watch_spec.lua
+++ /dev/null
@@ -1,195 +0,0 @@
-local helpers = require('test.functional.helpers')(after_each)
-local eq = helpers.eq
-local exec_lua = helpers.exec_lua
-local clear = helpers.clear
-local is_os = helpers.is_os
-local lfs = require('lfs')
-
-describe('vim._watch', function()
- before_each(function()
- clear()
- end)
-
- describe('watch', function()
- it('detects file changes', function()
- local root_dir = helpers.tmpname()
- os.remove(root_dir)
- lfs.mkdir(root_dir)
-
- local result = exec_lua(
- [[
- local root_dir = ...
-
- local events = {}
-
- local expected_events = 0
- local function wait_for_events()
- assert(vim.wait(100, function() return #events == expected_events end), 'Timed out waiting for expected number of events. Current events seen so far: ' .. vim.inspect(events))
- end
-
- local stop = vim._watch.watch(root_dir, {}, function(path, change_type)
- table.insert(events, { path = path, change_type = change_type })
- end)
-
- -- Only BSD seems to need some extra time for the watch to be ready to respond to events
- if vim.fn.has('bsd') then
- vim.wait(50)
- end
-
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
-
- expected_events = expected_events + 1
- wait_for_events()
-
- watched:close()
- os.remove(watched_path)
-
- expected_events = expected_events + 1
- wait_for_events()
-
- stop()
- -- No events should come through anymore
-
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
-
- vim.wait(50)
-
- watched:close()
- os.remove(watched_path)
-
- vim.wait(50)
-
- return events
- ]],
- root_dir
- )
-
- local expected = {
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir .. '/file',
- },
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]),
- path = root_dir .. '/file',
- },
- }
-
- -- kqueue only reports events on the watched path itself, so creating a file within a
- -- watched directory results in a "rename" libuv event on the directory.
- if is_os('bsd') then
- expected = {
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir,
- },
- {
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir,
- },
- }
- end
-
- eq(expected, result)
- end)
- end)
-
- describe('poll', function()
- it('detects file changes', function()
- local root_dir = helpers.tmpname()
- os.remove(root_dir)
- lfs.mkdir(root_dir)
-
- local result = exec_lua(
- [[
- local root_dir = ...
-
- local events = {}
-
- local poll_interval_ms = 1000
- local poll_wait_ms = poll_interval_ms+200
-
- local expected_events = 0
- local function wait_for_events()
- assert(vim.wait(poll_wait_ms, function() return #events == expected_events end), 'Timed out waiting for expected number of events. Current events seen so far: ' .. vim.inspect(events))
- end
-
- local stop = vim._watch.poll(root_dir, { interval = poll_interval_ms }, function(path, change_type)
- table.insert(events, { path = path, change_type = change_type })
- end)
-
- -- polling generates Created events for the existing entries when it starts.
- expected_events = expected_events + 1
- wait_for_events()
-
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
-
- expected_events = expected_events + 2
- wait_for_events()
-
- watched:close()
- os.remove(watched_path)
-
- expected_events = expected_events + 2
- wait_for_events()
-
- stop()
- -- No events should come through anymore
-
- local watched_path = root_dir .. '/file'
- local watched, err = io.open(watched_path, 'w')
- assert(not err, err)
-
- vim.wait(poll_wait_ms)
-
- watched:close()
- os.remove(watched_path)
-
- return events
- ]],
- root_dir
- )
-
- eq(5, #result)
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir,
- }, result[1])
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
- path = root_dir .. '/file',
- }, result[2])
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Changed]]),
- path = root_dir,
- }, result[3])
- -- The file delete and corresponding directory change events do not happen in any
- -- particular order, so allow either
- if result[4].path == root_dir then
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Changed]]),
- path = root_dir,
- }, result[4])
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]),
- path = root_dir .. '/file',
- }, result[5])
- else
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]),
- path = root_dir .. '/file',
- }, result[4])
- eq({
- change_type = exec_lua([[return vim._watch.FileChangeType.Changed]]),
- path = root_dir,
- }, result[5])
- end
- end)
- end)
-end)
diff --git a/test/functional/plugin/lsp/watchfiles_spec.lua b/test/functional/plugin/lsp/watchfiles_spec.lua
deleted file mode 100644
index c5d6803a7f..0000000000
--- a/test/functional/plugin/lsp/watchfiles_spec.lua
+++ /dev/null
@@ -1,173 +0,0 @@
-local helpers = require('test.functional.helpers')(after_each)
-
-local eq = helpers.eq
-local exec_lua = helpers.exec_lua
-local has_err = require('luassert').has.errors
-
-describe('vim.lsp._watchfiles', function()
- before_each(helpers.clear)
- after_each(helpers.clear)
-
- local match = function(...)
- return exec_lua('return require("vim.lsp._watchfiles")._match(...)', ...)
- end
-
- describe('glob matching', function()
- it('should match literal strings', function()
- eq(true, match('', ''))
- eq(false, match('', 'a'))
- eq(true, match('a', 'a'))
- eq(true, match('abc', 'abc'))
- eq(false, match('abc', 'abcdef'))
- eq(false, match('abc', 'a'))
- eq(false, match('a', 'b'))
- eq(false, match('.', 'a'))
- eq(true, match('$', '$'))
- eq(false, match('dir/subdir', 'dir/subdir/file'))
- end)
-
- it('should match * wildcards', function()
- -- eq(false, match('*', '')) -- TODO: this fails
- eq(true, match('*', 'a'))
- eq(false, match('*', '/a'))
- eq(false, match('*', 'a/'))
- eq(true, match('*', 'aaa'))
- eq(true, match('*.txt', 'file.txt'))
- eq(false, match('*.txt', 'file.txtxt'))
- eq(false, match('*.txt', 'dir/file.txt'))
- eq(false, match('*.txt', '/dir/file.txt'))
- eq(false, match('*.txt', 'C:/dir/file.txt'))
- eq(false, match('*.dir', 'test.dir/file'))
- eq(true, match('file.*', 'file.txt'))
- eq(false, match('file.*', 'not-file.txt'))
- eq(false, match('dir/*.txt', 'file.txt'))
- eq(true, match('dir/*.txt', 'dir/file.txt'))
- eq(false, match('dir/*.txt', 'dir/subdir/file.txt'))
- end)
-
- it('should match ? wildcards', function()
- eq(false, match('?', ''))
- eq(true, match('?', 'a'))
- eq(false, match('??', 'a'))
- eq(false, match('?', 'ab'))
- eq(true, match('??', 'ab'))
- eq(true, match('a?c', 'abc'))
- eq(false, match('a?c', 'a/c'))
- end)
-
- it('should match ** wildcards', function()
- eq(true, match('**', ''))
- eq(true, match('**', 'a'))
- eq(true, match('**', 'a/'))
- eq(true, match('**', '/a'))
- eq(true, match('**', 'C:/a'))
- eq(true, match('**', 'a/a'))
- eq(true, match('**', 'a/a/a'))
- eq(false, match('a**', ''))
- eq(true, match('a**', 'a'))
- eq(true, match('a**', 'abcd'))
- eq(false, match('a**', 'ba'))
- eq(false, match('a**', 'a/b'))
- eq(false, match('**a', ''))
- eq(true, match('**a', 'a'))
- eq(true, match('**a', 'dcba'))
- eq(false, match('**a', 'ab'))
- eq(false, match('**a', 'b/a'))
- eq(false, match('a/**', ''))
- eq(true, match('a/**', 'a'))
- eq(true, match('a/**', 'a/b'))
- eq(false, match('a/**', 'b/a'))
- eq(false, match('a/**', '/a'))
- eq(false, match('**/a', ''))
- eq(true, match('**/a', 'a'))
- eq(false, match('**/a', 'a/b'))
- eq(true, match('**/a', '/a'))
- eq(false, match('a/**/c', 'a'))
- eq(false, match('a/**/c', 'c'))
- eq(true, match('a/**/c', 'a/c'))
- eq(true, match('a/**/c', 'a/b/c'))
- eq(true, match('a/**/c', 'a/b/b/c'))
- eq(true, match('**/a/**', 'a'))
- eq(true, match('**/a/**', '/dir/a'))
- eq(true, match('**/a/**', 'a/dir'))
- eq(true, match('**/a/**', 'dir/a/dir'))
- eq(true, match('**/a/**', '/a/dir'))
- eq(true, match('**/a/**', 'C:/a/dir'))
- -- eq(false, match('**/a/**', 'a.txt')) -- TODO: this fails
- end)
-
- it('should match {} groups', function()
- eq(false, match('{}', ''))
- eq(true, match('{,}', ''))
- eq(false, match('{}', 'a'))
- eq(true, match('{a}', 'a'))
- eq(false, match('{a}', 'aa'))
- eq(false, match('{a}', 'ab'))
- eq(false, match('{ab}', 'a'))
- eq(true, match('{ab}', 'ab'))
- eq(true, match('{a,b}', 'a'))
- eq(true, match('{a,b}', 'b'))
- eq(false, match('{a,b}', 'ab'))
- eq(true, match('{ab,cd}', 'ab'))
- eq(false, match('{ab,cd}', 'a'))
- eq(true, match('{ab,cd}', 'cd'))
- eq(true, match('{a,b,c}', 'c'))
- eq(false, match('{a,{b,c}}', 'c')) -- {} can't nest
- end)
-
- it('should match [] groups', function()
- eq(true, match('[]', ''))
- eq(false, match('[a-z]', ''))
- eq(true, match('[a-z]', 'a'))
- eq(false, match('[a-z]', 'ab'))
- eq(true, match('[a-z]', 'z'))
- eq(true, match('[a-z]', 'j'))
- eq(false, match('[a-f]', 'j'))
- eq(false, match('[a-z]', '`')) -- 'a' - 1
- eq(false, match('[a-z]', '{')) -- 'z' + 1
- eq(false, match('[a-z]', 'A'))
- eq(false, match('[a-z]', '5'))
- eq(true, match('[A-Z]', 'A'))
- eq(true, match('[A-Z]', 'Z'))
- eq(true, match('[A-Z]', 'J'))
- eq(false, match('[A-Z]', '@')) -- 'A' - 1
- eq(false, match('[A-Z]', '[')) -- 'Z' + 1
- eq(false, match('[A-Z]', 'a'))
- eq(false, match('[A-Z]', '5'))
- eq(true, match('[a-zA-Z0-9]', 'z'))
- eq(true, match('[a-zA-Z0-9]', 'Z'))
- eq(true, match('[a-zA-Z0-9]', '9'))
- eq(false, match('[a-zA-Z0-9]', '&'))
- end)
-
- it('should match [!...] groups', function()
- has_err(function() match('[!]', '') end) -- not a valid pattern
- eq(false, match('[!a-z]', ''))
- eq(false, match('[!a-z]', 'a'))
- eq(false, match('[!a-z]', 'z'))
- eq(false, match('[!a-z]', 'j'))
- eq(true, match('[!a-f]', 'j'))
- eq(false, match('[!a-f]', 'jj'))
- eq(true, match('[!a-z]', '`')) -- 'a' - 1
- eq(true, match('[!a-z]', '{')) -- 'z' + 1
- eq(false, match('[!a-zA-Z0-9]', 'a'))
- eq(false, match('[!a-zA-Z0-9]', 'A'))
- eq(false, match('[!a-zA-Z0-9]', '0'))
- eq(true, match('[!a-zA-Z0-9]', '!'))
- end)
-
- it('should match complex patterns', function()
- eq(false, match('**/*.{c,h}', ''))
- eq(false, match('**/*.{c,h}', 'c'))
- eq(true, match('**/*.{c,h}', 'file.c'))
- eq(true, match('**/*.{c,h}', 'file.h'))
- eq(true, match('**/*.{c,h}', '/file.c'))
- eq(true, match('**/*.{c,h}', 'dir/subdir/file.c'))
- eq(true, match('**/*.{c,h}', 'dir/subdir/file.h'))
-
- eq(true, match('{[0-9],[a-z]}', '0'))
- eq(true, match('{[0-9],[a-z]}', 'a'))
- eq(false, match('{[0-9],[a-z]}', 'A'))
- end)
- end)
-end)
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index 5eb367b0b8..f1aad08140 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -1,6 +1,5 @@
local helpers = require('test.functional.helpers')(after_each)
local lsp_helpers = require('test.functional.plugin.lsp.helpers')
-local lfs = require('lfs')
local assert_log = helpers.assert_log
local buf_lines = helpers.buf_lines
@@ -3590,424 +3589,4 @@ describe('LSP', function()
eq(expected, result)
end)
end)
-
- describe('vim.lsp._watchfiles', function()
- it('sends notifications when files change', function()
- local root_dir = helpers.tmpname()
- os.remove(root_dir)
- lfs.mkdir(root_dir)
-
- exec_lua(create_server_definition)
- local result = exec_lua([[
- local root_dir = ...
-
- local server = _create_server()
- local client_id = vim.lsp.start({
- name = 'watchfiles-test',
- cmd = server.cmd,
- root_dir = root_dir,
- })
-
- local expected_messages = 2 -- initialize, initialized
-
- local msg_wait_timeout = require('vim.lsp._watchfiles')._watchfunc == vim._watch.poll and 2500 or 200
- local function wait_for_messages()
- assert(vim.wait(msg_wait_timeout, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
- end
-
- wait_for_messages()
-
- vim.lsp.handlers['client/registerCapability'](nil, {
- registrations = {
- {
- id = 'watchfiles-test-0',
- method = 'workspace/didChangeWatchedFiles',
- registerOptions = {
- watchers = {
- {
- globPattern = '**/watch',
- kind = 7,
- },
- },
- },
- },
- },
- }, { client_id = client_id })
-
- local path = root_dir .. '/watch'
- local file = io.open(path, 'w')
- file:close()
-
- expected_messages = expected_messages + 1
- wait_for_messages()
-
- os.remove(path)
-
- expected_messages = expected_messages + 1
- wait_for_messages()
-
- return server.messages
- ]], root_dir)
-
- local function watched_uri(fname)
- return exec_lua([[
- local root_dir, fname = ...
- return vim.uri_from_fname(root_dir .. '/' .. fname)
- ]], root_dir, fname)
- end
-
- eq(4, #result)
- eq('workspace/didChangeWatchedFiles', result[3].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('watch'),
- },
- },
- }, result[3].params)
- eq('workspace/didChangeWatchedFiles', result[4].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
- uri = watched_uri('watch'),
- },
- },
- }, result[4].params)
- end)
-
- it('correctly registers and unregisters', function()
- local root_dir = 'some_dir'
- exec_lua(create_server_definition)
- local result = exec_lua([[
- local root_dir = ...
-
- local server = _create_server()
- local client_id = vim.lsp.start({
- name = 'watchfiles-test',
- cmd = server.cmd,
- root_dir = root_dir,
- })
-
- local expected_messages = 2 -- initialize, initialized
- local function wait_for_messages()
- assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
- end
-
- wait_for_messages()
-
- local send_event
- require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
- local stoppped = false
- send_event = function(...)
- if not stoppped then
- callback(...)
- end
- end
- return function()
- stoppped = true
- end
- end
-
- vim.lsp.handlers['client/registerCapability'](nil, {
- registrations = {
- {
- id = 'watchfiles-test-0',
- method = 'workspace/didChangeWatchedFiles',
- registerOptions = {
- watchers = {
- {
- globPattern = '**/*.watch0',
- },
- },
- },
- },
- },
- }, { client_id = client_id })
-
- send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created)
- send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created)
-
- expected_messages = expected_messages + 1
- wait_for_messages()
-
- vim.lsp.handlers['client/registerCapability'](nil, {
- registrations = {
- {
- id = 'watchfiles-test-1',
- method = 'workspace/didChangeWatchedFiles',
- registerOptions = {
- watchers = {
- {
- globPattern = '**/*.watch1',
- },
- },
- },
- },
- },
- }, { client_id = client_id })
-
- vim.lsp.handlers['client/unregisterCapability'](nil, {
- unregisterations = {
- {
- id = 'watchfiles-test-0',
- method = 'workspace/didChangeWatchedFiles',
- },
- },
- }, { client_id = client_id })
-
- send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created)
- send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created)
-
- expected_messages = expected_messages + 1
- wait_for_messages()
-
- return server.messages
- ]], root_dir)
-
- local function watched_uri(fname)
- return exec_lua([[
- local root_dir, fname = ...
- return vim.uri_from_fname(root_dir .. '/' .. fname)
- ]], root_dir, fname)
- end
-
- eq(4, #result)
- eq('workspace/didChangeWatchedFiles', result[3].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('file.watch0'),
- },
- },
- }, result[3].params)
- eq('workspace/didChangeWatchedFiles', result[4].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('file.watch1'),
- },
- },
- }, result[4].params)
- end)
-
- it('correctly handles the registered watch kind', function()
- local root_dir = 'some_dir'
- exec_lua(create_server_definition)
- local result = exec_lua([[
- local root_dir = ...
-
- local server = _create_server()
- local client_id = vim.lsp.start({
- name = 'watchfiles-test',
- cmd = server.cmd,
- root_dir = root_dir,
- })
-
- local expected_messages = 2 -- initialize, initialized
- local function wait_for_messages()
- assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
- end
-
- wait_for_messages()
-
- local watch_callbacks = {}
- local function send_event(...)
- for _, cb in ipairs(watch_callbacks) do
- cb(...)
- end
- end
- require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
- table.insert(watch_callbacks, callback)
- return function()
- -- noop because this test never stops the watch
- end
- end
-
- local protocol = require('vim.lsp.protocol')
-
- local watchers = {}
- local max_kind = protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete
- for i = 0, max_kind do
- local j = i
- table.insert(watchers, {
- globPattern = {
- baseUri = vim.uri_from_fname('/dir'..tostring(i)),
- pattern = 'watch'..tostring(i),
- },
- kind = i,
- })
- end
- vim.lsp.handlers['client/registerCapability'](nil, {
- registrations = {
- {
- id = 'watchfiles-test-kind',
- method = 'workspace/didChangeWatchedFiles',
- registerOptions = {
- watchers = watchers,
- },
- },
- },
- }, { client_id = client_id })
-
- for i = 0, max_kind do
- local filename = 'watch'..tostring(i)
- send_event(filename, vim._watch.FileChangeType.Created)
- send_event(filename, vim._watch.FileChangeType.Changed)
- send_event(filename, vim._watch.FileChangeType.Deleted)
- end
-
- expected_messages = expected_messages + 1
- wait_for_messages()
-
- return server.messages
- ]], root_dir)
-
- local function watched_uri(fname)
- return exec_lua([[
- return vim.uri_from_fname(...)
- ]], fname)
- end
-
- eq(3, #result)
- eq('workspace/didChangeWatchedFiles', result[3].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('watch1'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
- uri = watched_uri('watch2'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('watch3'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
- uri = watched_uri('watch3'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
- uri = watched_uri('watch4'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('watch5'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
- uri = watched_uri('watch5'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
- uri = watched_uri('watch6'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
- uri = watched_uri('watch6'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('watch7'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
- uri = watched_uri('watch7'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]),
- uri = watched_uri('watch7'),
- },
- },
- }, result[3].params)
- end)
-
- it('prunes duplicate events', function()
- local root_dir = 'some_dir'
- exec_lua(create_server_definition)
- local result = exec_lua([[
- local root_dir = ...
-
- local server = _create_server()
- local client_id = vim.lsp.start({
- name = 'watchfiles-test',
- cmd = server.cmd,
- root_dir = root_dir,
- })
-
- local expected_messages = 2 -- initialize, initialized
- local function wait_for_messages()
- assert(vim.wait(200, function() return #server.messages == expected_messages end), 'Timed out waiting for expected number of messages. Current messages seen so far: ' .. vim.inspect(server.messages))
- end
-
- wait_for_messages()
-
- local send_event
- require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback)
- send_event = callback
- return function()
- -- noop because this test never stops the watch
- end
- end
-
- vim.lsp.handlers['client/registerCapability'](nil, {
- registrations = {
- {
- id = 'watchfiles-test-kind',
- method = 'workspace/didChangeWatchedFiles',
- registerOptions = {
- watchers = {
- {
- globPattern = '**/*',
- },
- },
- },
- },
- },
- }, { client_id = client_id })
-
- send_event('file1', vim._watch.FileChangeType.Created)
- send_event('file1', vim._watch.FileChangeType.Created) -- pruned
- send_event('file1', vim._watch.FileChangeType.Changed)
- send_event('file2', vim._watch.FileChangeType.Created)
- send_event('file1', vim._watch.FileChangeType.Changed) -- pruned
-
- expected_messages = expected_messages + 1
- wait_for_messages()
-
- return server.messages
- ]], root_dir)
-
- local function watched_uri(fname)
- return exec_lua([[
- return vim.uri_from_fname(...)
- ]], fname)
- end
-
- eq(3, #result)
- eq('workspace/didChangeWatchedFiles', result[3].method)
- eq({
- changes = {
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('file1'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]),
- uri = watched_uri('file1'),
- },
- {
- type = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]),
- uri = watched_uri('file2'),
- },
- },
- }, result[3].params)
- end)
- end)
end)