From 5732aa706c639b3d775573d91d1139f24624629c Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Sat, 25 Feb 2023 03:07:18 -0600 Subject: feat(lsp): implement workspace/didChangeWatchedFiles (#21293) --- runtime/lua/vim/lsp/_watchfiles.lua | 274 ++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 runtime/lua/vim/lsp/_watchfiles.lua (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua new file mode 100644 index 0000000000..b9268b963c --- /dev/null +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -0,0 +1,274 @@ +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> client id -> registration id -> cancel function +local cancels = vim.defaulttable() + +local queue_timeout_ms = 100 +---@type table client id -> libuv timer which will send queued changes at its timeout +local queue_timers = {} +---@type table client id -> set of queued changes to send in a single LSP notification +local change_queues = {} +---@type table> 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 -- cgit From f0f27e9aef7c237dd55fbb5c2cd47c2f42d01742 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Sat, 25 Feb 2023 11:17:28 +0100 Subject: Revert "feat(lsp): implement workspace/didChangeWatchedFiles (#21293)" This reverts commit 5732aa706c639b3d775573d91d1139f24624629c. Causes editor to freeze in projects with many watcher registrations --- runtime/lua/vim/lsp/_watchfiles.lua | 274 ------------------------------------ 1 file changed, 274 deletions(-) delete mode 100644 runtime/lua/vim/lsp/_watchfiles.lua (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') 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> client id -> registration id -> cancel function -local cancels = vim.defaulttable() - -local queue_timeout_ms = 100 ----@type table client id -> libuv timer which will send queued changes at its timeout -local queue_timers = {} ----@type table client id -> set of queued changes to send in a single LSP notification -local change_queues = {} ----@type table> 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 -- cgit From ac69ba5fa0081026f2c5e6e29d5788802479b7b9 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Sun, 5 Mar 2023 00:52:27 -0600 Subject: feat(lsp): implement workspace/didChangeWatchedFiles (#22405) --- runtime/lua/vim/lsp/_watchfiles.lua | 293 ++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 runtime/lua/vim/lsp/_watchfiles.lua (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua new file mode 100644 index 0000000000..96d7fa1d35 --- /dev/null +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -0,0 +1,293 @@ +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> client id -> registration id -> cancel function +local cancels = vim.defaulttable() + +local queue_timeout_ms = 100 +---@type table client id -> libuv timer which will send queued changes at its timeout +local queue_timers = {} +---@type table client id -> set of queued changes to send in a single LSP notification +local change_queues = {} +---@type table> 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) + local watch_regs = {} + 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(watch_regs, { + base_dir = base_dir, + pattern = pattern, + kind = kind, + }) + end + end + + local callback = function(base_dir) + return function(fullpath, change_type) + for _, w in ipairs(watch_regs) 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 base_dir == w.base_dir and 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 + + local watching = {} + for _, w in ipairs(watch_regs) do + if not watching[w.base_dir] then + watching[w.base_dir] = true + table.insert( + cancels[client_id][reg.id], + M._watchfunc(w.base_dir, { uvflags = { recursive = true } }, callback(w.base_dir)) + ) + 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 -- cgit From ed05d38d9fa643c7e562b754c6cfed8b9da5c4d8 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 5 Mar 2023 08:42:15 +0100 Subject: fix(lsp): don't monitor files if workspace_folders is nil (#22531) Fixes: Error SERVER_REQUEST_HANDLER_ERROR: "...di/dev/neovim/neovim/runtime/lua/vim/lsp/_watchfiles.lua :200: bad argument #1 to 'ipairs' (table expected, got nil)" Language servers can be started without root_dir or workspace_folders. --- runtime/lua/vim/lsp/_watchfiles.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 96d7fa1d35..533a955925 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -168,11 +168,11 @@ end M._watchfunc = (vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1) and watch.watch or watch.poll ----@type table> client id -> registration id -> cancel function +---@type table> client id -> registration id -> cancel function local cancels = vim.defaulttable() local queue_timeout_ms = 100 ----@type table client id -> libuv timer which will send queued changes at its timeout +---@type table client id -> libuv timer which will send queued changes at its timeout local queue_timers = {} ---@type table client id -> set of queued changes to send in a single LSP notification local change_queues = {} @@ -193,6 +193,9 @@ local to_lsp_change_type = { function M.register(reg, ctx) local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) + if not client.workspace_folders then + return + end local watch_regs = {} for _, w in ipairs(reg.registerOptions.watchers) do local glob_patterns = {} -- cgit From 075a72d5ff9d49b1e93c0253b54931ecdcf673f3 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Tue, 9 May 2023 11:12:54 -0500 Subject: fix(lsp): fix relative patterns for `workspace/didChangeWatchedFiles` (#23548) --- runtime/lua/vim/lsp/_watchfiles.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 533a955925..3a78724d79 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -198,16 +198,17 @@ function M.register(reg, ctx) end local watch_regs = {} for _, w in ipairs(reg.registerOptions.watchers) do + local relative_pattern = false 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 + relative_pattern = true 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 @@ -216,9 +217,16 @@ function M.register(reg, ctx) 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 + local pattern = glob_pattern.pattern + if relative_pattern then + pattern = base_dir .. '/' .. pattern + end + pattern = parse(pattern) + table.insert(watch_regs, { base_dir = base_dir, pattern = pattern, -- cgit From 073035a030f5ef8ae9b9ca51552f887944c52eaa Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Sat, 20 May 2023 00:45:39 -0500 Subject: fix(lsp): don't register didChangeWatchedFiles when capability not set (#23689) Some LSP servers (tailwindcss, rome) are known to request registration for `workspace/didChangeWatchedFiles` even when the corresponding client capability does not advertise support. This change adds an extra check in the `client/registerCapability` handler not to start a watch unless the client capability is set appropriately. --- runtime/lua/vim/lsp/_watchfiles.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 3a78724d79..cf2c57db1f 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -193,7 +193,12 @@ local to_lsp_change_type = { function M.register(reg, ctx) local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) - if not client.workspace_folders then + 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 = {} -- cgit From 416fe8d185dcc072df7942953d867d9c605e9ffd Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Mon, 5 Jun 2023 00:19:31 -0500 Subject: refactor(lsp): use LPeg for watchfiles matching (#23788) --- runtime/lua/vim/lsp/_watchfiles.lua | 191 +++++++++++------------------------- 1 file changed, 56 insertions(+), 135 deletions(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index cf2c57db1f..14e5dc6cf8 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -1,152 +1,81 @@ local bit = require('bit') +local lpeg = require('lpeg') 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. +--- 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 table A list of Lua patterns. A match with any of them matches the input glob pattern. +---@return userdata An |lpeg| representation of the pattern, or nil if the pattern is invalid. local function parse(pattern) - local patterns = { '' } + local l = lpeg - local path_sep = '[/\\]' - local non_path_sep = '[^/\\]' + local P, S, V = lpeg.P, lpeg.S, lpeg.V + local C, Cc, Ct, Cf = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cf - 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 pathsep = '/' - 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 + local function class(inv, ranges) + for i, r in ipairs(ranges) do + ranges[i] = r[1] .. r[2] end - if segment ~= '' then - table.insert(segments, segment) + local patt = l.R(unpack(ranges)) + if inv == '!' then + patt = P(1) - patt end - return segments + return patt 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 + local function add(acc, a) + return acc + a 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 + local function mul(acc, m) + return acc * m + end - if not last_seg and (segments[i + 1] ~= '**' or i + 1 < #segments) then - append({ path_sep }) - end - end + local function star(stars, after) + return (-after * (l.P(1) - pathsep)) ^ #stars * after end - return patterns + 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 ---- 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. @@ -155,15 +84,7 @@ 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 + 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 @@ -226,11 +147,11 @@ function M.register(reg, ctx) local kind = w.kind or protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete - local pattern = glob_pattern.pattern + local pattern = parse(glob_pattern.pattern) + assert(pattern, 'invalid pattern: ' .. glob_pattern.pattern) if relative_pattern then - pattern = base_dir .. '/' .. pattern + pattern = lpeg.P(base_dir .. '/') * pattern end - pattern = parse(pattern) table.insert(watch_regs, { base_dir = base_dir, -- cgit From 79a5b89d66db74560e751561542064674e980146 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Wed, 14 Jun 2023 05:40:11 -0500 Subject: perf(lsp): reduce polling handles for workspace/didChangeWatchedFiles (#23500) Co-authored-by: Lewis Russell --- runtime/lua/vim/lsp/_watchfiles.lua | 52 +++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 14e5dc6cf8..87938fe4d5 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -1,7 +1,7 @@ local bit = require('bit') -local lpeg = require('lpeg') local watch = require('vim._watch') local protocol = require('vim.lsp.protocol') +local lpeg = vim.lpeg local M = {} @@ -107,6 +107,13 @@ local to_lsp_change_type = { [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. @@ -122,10 +129,10 @@ function M.register(reg, ctx) then return end - local watch_regs = {} + local watch_regs = {} --- @type table for _, w in ipairs(reg.registerOptions.watchers) do local relative_pattern = false - local glob_patterns = {} + 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 }) @@ -135,7 +142,7 @@ function M.register(reg, ctx) table.insert(glob_patterns, w.globPattern) end for _, glob_pattern in ipairs(glob_patterns) do - local base_dir = nil + 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 @@ -144,6 +151,7 @@ function M.register(reg, ctx) 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 @@ -153,8 +161,8 @@ function M.register(reg, ctx) pattern = lpeg.P(base_dir .. '/') * pattern end - table.insert(watch_regs, { - base_dir = base_dir, + watch_regs[base_dir] = watch_regs[base_dir] or {} + table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind, }) @@ -163,12 +171,12 @@ function M.register(reg, ctx) local callback = function(base_dir) return function(fullpath, change_type) - for _, w in ipairs(watch_regs) do + 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 base_dir == w.base_dir and M._match(w.pattern, fullpath) and change_type_match then + if M._match(w.pattern, fullpath) and change_type_match then local change = { uri = vim.uri_from_fname(fullpath), type = change_type, @@ -198,15 +206,25 @@ function M.register(reg, ctx) end end - local watching = {} - for _, w in ipairs(watch_regs) do - if not watching[w.base_dir] then - watching[w.base_dir] = true - table.insert( - cancels[client_id][reg.id], - M._watchfunc(w.base_dir, { uvflags = { recursive = true } }, callback(w.base_dir)) - ) - 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 -- cgit From be74807eef13ff8c90d55cf8b22b01d6d33b1641 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 18 Jul 2023 15:42:30 +0100 Subject: docs(lua): more improvements (#24387) * 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 --------- Co-authored-by: Justin M. Keyes --- runtime/lua/vim/lsp/_watchfiles.lua | 1 - 1 file changed, 1 deletion(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 87938fe4d5..b66f2f6f32 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -5,7 +5,6 @@ local lpeg = vim.lpeg local M = {} ----@private --- 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. --- -- cgit From f1772272b4fda43c093fc495f54b5e7c11968d62 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 3 Aug 2023 19:03:48 +0800 Subject: refactor(lsp): use protocol.Methods instead of strings #24537 --- runtime/lua/vim/lsp/_watchfiles.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index b66f2f6f32..c271dc6e14 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -1,6 +1,7 @@ local bit = require('bit') local watch = require('vim._watch') local protocol = require('vim.lsp.protocol') +local ms = protocol.Methods local lpeg = vim.lpeg local M = {} @@ -190,7 +191,7 @@ function M.register(reg, ctx) if not queue_timers[client_id] then queue_timers[client_id] = vim.defer_fn(function() - client.notify('workspace/didChangeWatchedFiles', { + client.notify(ms.workspace_didChangeWatchedFiles, { changes = change_queues[client_id], }) queue_timers[client_id] = nil -- cgit From cc87dda31a5b5637ade7ddcfe5199f2df5fd47df Mon Sep 17 00:00:00 2001 From: Sean Dewar Date: Fri, 4 Aug 2023 07:10:54 +0100 Subject: fix(lsp): do not assume client capability exists in watchfiles check (#24550) PR #23689 assumes `client.config.capabilities.workspace.didChangeWatchedFiles` exists when checking `dynamicRegistration`, but thats's true only if it was passed to `vim.lsp.start{_client}`. This caused #23806 (still an issue in v0.9.1; needs manual backport), but #23681 fixed it by defaulting `config.capabilities` to `make_client_capabilities` if not passed to `vim.lsp.start{_client}`. However, the bug resurfaces on HEAD if you provide a non-nil `capabilities` to `vim.lsp.start{_client}` with missing fields (e.g: not made via `make_client_capabilities`). From what I see, the spec says such missing fields should be interpreted as an absence of the capability (including those indicated by missing sub-fields): https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities Also, suggest `vim.empty_dict()` for an empty dict in `:h vim.lsp.start_client()` (`{[vim.type_idx]=vim.types.dictionary}` no longer works anyway, probably since the cjson switch). --- runtime/lua/vim/lsp/_watchfiles.lua | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index c271dc6e14..5dbd4a7199 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -121,12 +121,15 @@ M._poll_exclude_pattern = parse('**/.git/{objects,subtree-cache}/**') 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 + -- 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. + local has_capability = vim.tbl_get( + client.config.capabilities or {}, + 'workspace', + 'didChangeWatchedFiles', + 'dynamicRegistration' + ) + if not has_capability or not client.workspace_folders then return end local watch_regs = {} --- @type table -- cgit From c43c745a14dced87a23227d7be4f1c33d4455193 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Wed, 9 Aug 2023 11:06:13 +0200 Subject: fix(lua): improve annotations for stricter luals diagnostics (#24609) Problem: luals returns stricter diagnostics with bundled luarc.json Solution: Improve some function and type annotations: * use recognized uv.* types * disable diagnostic for global `vim` in shared.lua * docs: don't start comment lines with taglink (otherwise LuaLS will interpret it as a type) * add type alias for lpeg pattern * fix return annotation for `vim.secure.trust` * rename local Range object in vim.version (shadows `Range` in vim.treesitter) * fix some "missing fields" warnings * add missing required fields for test functions in eval.lua * rename lsp meta files for consistency --- runtime/lua/vim/lsp/_watchfiles.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 5dbd4a7199..1a4909b3f3 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -6,11 +6,13 @@ local lpeg = vim.lpeg local M = {} +---@alias lpeg userdata + --- 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. +---@return lpeg An |lpeg| representation of the pattern, or nil if the pattern is invalid. local function parse(pattern) local l = lpeg @@ -109,7 +111,7 @@ local to_lsp_change_type = { --- 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 +---@type lpeg parsed Lpeg pattern M._poll_exclude_pattern = parse('**/.git/{objects,subtree-cache}/**') + parse('**/node_modules/*/**') + parse('**/.hg/store/**') @@ -132,7 +134,7 @@ function M.register(reg, ctx) if not has_capability or not client.workspace_folders then return end - local watch_regs = {} --- @type table + local watch_regs = {} --- @type table for _, w in ipairs(reg.registerOptions.watchers) do local relative_pattern = false local glob_patterns = {} --- @type {baseUri:string, pattern: string}[] -- cgit From ec79ff893d5906e1f0d90953cffa535ffae47823 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 20 Nov 2023 17:19:41 +0100 Subject: refactor(lsp): add type annotations to _watchfiles (#26109) --- runtime/lua/vim/lsp/_watchfiles.lua | 108 ++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 55 deletions(-) (limited to 'runtime/lua/vim/lsp/_watchfiles.lua') diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 1a4909b3f3..1fd112631d 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -6,13 +6,11 @@ local lpeg = vim.lpeg local M = {} ----@alias lpeg userdata - --- 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 lpeg An |lpeg| representation of the pattern, or nil if the pattern is invalid. +---@return vim.lpeg.Pattern? pattern An |lpeg| representation of the pattern, or nil if the pattern is invalid. local function parse(pattern) local l = lpeg @@ -73,36 +71,38 @@ local function parse(pattern) End = P(-1) * Cc(l.P(-1)), }) - return p:match(pattern) + return p:match(pattern) --[[@as vim.lpeg.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 pattern string|vim.lpeg.Pattern 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) + local p = assert(parse(pattern)) + return p:match(s) ~= nil 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> client id -> registration id -> cancel function +---@type table> client id -> registration id -> cancel function local cancels = vim.defaulttable() local queue_timeout_ms = 100 ----@type table client id -> libuv timer which will send queued changes at its timeout +---@type table client id -> libuv timer which will send queued changes at its timeout local queue_timers = {} ----@type table client id -> set of queued changes to send in a single LSP notification +---@type table client id -> set of queued changes to send in a single LSP notification local change_queues = {} ----@type table> client id -> URI -> last type of change processed +---@type table> 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() +---@type table local to_lsp_change_type = { [watch.FileChangeType.Created] = protocol.FileChangeType.Created, [watch.FileChangeType.Changed] = protocol.FileChangeType.Changed, @@ -111,18 +111,18 @@ local to_lsp_change_type = { --- 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 parsed Lpeg pattern +---@type vim.lpeg.Pattern parsed 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|. +---@param reg lsp.Registration LSP Registration object. +---@param ctx lsp.HandlerContext 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) + local client = assert(vim.lsp.get_client_by_id(client_id), 'Client must be running') -- 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. local has_capability = vim.tbl_get( @@ -134,57 +134,53 @@ function M.register(reg, ctx) if not has_capability or not client.workspace_folders then return end - local watch_regs = {} --- @type table - 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 + local register_options = reg.registerOptions --[[@as lsp.DidChangeWatchedFilesRegistrationOptions]] + ---@type table by base_dir + local watch_regs = vim.defaulttable() + for _, w in ipairs(register_options.watchers) do + local kind = w.kind + or (protocol.WatchKind.Create + protocol.WatchKind.Change + protocol.WatchKind.Delete) + local glob_pattern = w.globPattern + + if type(glob_pattern) == 'string' then + local pattern = parse(glob_pattern) + if not pattern then + error('Cannot parse pattern: ' .. glob_pattern) + end for _, folder in ipairs(client.workspace_folders) do - table.insert(glob_patterns, { baseUri = folder.uri, pattern = w.globPattern }) + local base_dir = vim.uri_to_fname(folder.uri) + table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind }) 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 base_uri = glob_pattern.baseUri + local uri = type(base_uri) == 'string' and base_uri or base_uri.uri + local base_dir = vim.uri_to_fname(uri) local pattern = parse(glob_pattern.pattern) - assert(pattern, 'invalid pattern: ' .. glob_pattern.pattern) - if relative_pattern then - pattern = lpeg.P(base_dir .. '/') * pattern + if not pattern then + error('Cannot parse pattern: ' .. glob_pattern.pattern) end - - watch_regs[base_dir] = watch_regs[base_dir] or {} - table.insert(watch_regs[base_dir], { - pattern = pattern, - kind = kind, - }) + pattern = lpeg.P(base_dir .. '/') * pattern + table.insert(watch_regs[base_dir], { pattern = pattern, kind = kind }) end end + ---@param base_dir string 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] + local registrations = watch_regs[base_dir] + for _, w in ipairs(registrations) do + local lsp_change_type = assert( + to_lsp_change_type[change_type], + 'Must receive change type Created, Changed or Deleted' + ) -- e.g. match kind with Delete bit (0b0100) to Delete change_type (3) - local kind_mask = bit.lshift(1, change_type - 1) + local kind_mask = bit.lshift(1, lsp_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 + if w.pattern:match(fullpath) ~= nil and change_type_match then + ---@type lsp.FileEvent local change = { uri = vim.uri_from_fname(fullpath), - type = change_type, + type = lsp_change_type, } local last_type = change_cache[client_id][change.uri] @@ -196,9 +192,11 @@ function M.register(reg, ctx) if not queue_timers[client_id] then queue_timers[client_id] = vim.defer_fn(function() - client.notify(ms.workspace_didChangeWatchedFiles, { + ---@type lsp.DidChangeWatchedFilesParams + local params = { changes = change_queues[client_id], - }) + } + client.notify(ms.workspace_didChangeWatchedFiles, params) queue_timers[client_id] = nil change_queues[client_id] = nil change_cache[client_id] = nil @@ -235,8 +233,8 @@ end --- Unregisters the workspace/didChangeWatchedFiles capability dynamically. --- ----@param unreg table LSP Unregistration object. ----@param ctx table Context from the |lsp-handler|. +---@param unreg lsp.Unregistration LSP Unregistration object. +---@param ctx lsp.HandlerContext Context from the |lsp-handler|. function M.unregister(unreg, ctx) local client_id = ctx.client_id local client_cancels = cancels[client_id] -- cgit