diff --git a/lua/luasnip/loaders/from_lua.lua b/lua/luasnip/loaders/from_lua.lua index 741871bcb..3f896788d 100644 --- a/lua/luasnip/loaders/from_lua.lua +++ b/lua/luasnip/loaders/from_lua.lua @@ -148,7 +148,8 @@ local Collection = {} local Collection_mt = { __index = Collection } -function Collection:new(root, lazy, include_ft, exclude_ft, add_opts) + +function Collection.new(root, lazy, include_ft, exclude_ft, add_opts, lazy_watcher) local ft_filter = loader_util.ft_filter(include_ft, exclude_ft) local o = setmetatable({ root = root, @@ -170,7 +171,7 @@ function Collection:new(root, lazy, include_ft, exclude_ft, add_opts) }, Collection_mt) -- only register files up to a depth of 2. - o.watcher = tree_watcher(root, 2, { + local watcher_ok, err = pcall(tree_watcher, root, 2, { -- don't handle removals for now. new_file = function(path) vim.schedule_wrap(function() @@ -185,7 +186,11 @@ function Collection:new(root, lazy, include_ft, exclude_ft, add_opts) o:reload(path) end)() end - }) + }, {lazy = lazy_watcher}) + + if not watcher_ok then + error(("Could not create watcher: %s"):format(err)) + end log.info("Initialized snippet-collection at `%s`", root) @@ -271,12 +276,24 @@ local function _load(lazy, opts) local add_opts = loader_util.make_add_opts(opts) local include = opts.include local exclude = opts.exclude + local lazy_paths = opts.lazy_paths or {} + + local collection_roots = loader_util.resolve_root_paths(paths, "luasnippets") + local lazy_roots = loader_util.resolve_lazy_root_paths(lazy_paths) - local collection_roots = loader_util.resolve_root_paths(opts.paths, "luasnippets") log.info("Found roots `%s` for paths `%s`.", vim.inspect(collection_roots), vim.inspect(paths)) + log.info("Determined roots `%s` for lazy_paths `%s`.", vim.inspect(lazy_roots), vim.inspect(lazy_paths)) - for _, collection_root in ipairs(collection_roots) do - table.insert(M.collections, Collection:new(collection_root, lazy, include, exclude, add_opts)) + for paths_lazy, roots in pairs({[true] = lazy_roots, [false] = collection_roots}) do + for _, collection_root in ipairs(roots) do + local ok, coll_or_err = pcall(Collection.new, collection_root, lazy, include, exclude, add_opts, paths_lazy) + + if not ok then + log.error("Could not create collection at %s: %s", collection_root, coll_or_err) + else + table.insert(M.collections, coll_or_err) + end + end end end diff --git a/lua/luasnip/loaders/tree_watcher.lua b/lua/luasnip/loaders/tree_watcher.lua index 70b3180af..a4b27e0b2 100644 --- a/lua/luasnip/loaders/tree_watcher.lua +++ b/lua/luasnip/loaders/tree_watcher.lua @@ -11,10 +11,91 @@ local TreeWatcher_mt = { } function TreeWatcher:stop_recursive() for _, child_watcher in ipairs(self.dir_watchers) do - child_watcher.fs_event:stop() + child_watcher:stop() end + self:stop() +end + +function TreeWatcher:stop() self.fs_event:stop() end +function TreeWatcher:start() + if self.depth == 0 then + -- don't watch children for 0-depth. + return + end + + log.info("start monitoring directory %s", self.root) + + -- does not work on nfs-drive, at least if it's edited from another + -- machine. + self.fs_event:start(self.root, {}, function(err, relpath, events) + if self.removed then + return + end + vim.schedule_wrap(function() + log.debug("raw: self.root: %s; err: %s; relpath: %s; change: %s; rename: %s", self.root, err, relpath, events.change, events.rename) + local full_path = Path.join(self.root, relpath) + local path_stat = uv.fs_stat(full_path) + + -- try to figure out what happened in the directory. + if events.rename then + if not uv.fs_stat(self.root) then + self:remove_root() + return + end + if not path_stat then + self:remove_child(relpath, full_path) + return + end + + local f_type + if path_stat.type == "link" then + f_type = uv.fs_stat(uv.fs_realpath(full_path)) + else + f_type = path_stat.type + end + + if f_type == "file" then + self:new_file(relpath, full_path) + return + elseif f_type == "directory" then + self:new_dir(relpath, full_path) + return + end + elseif events.change then + self:change_child(relpath, full_path) + end + end)() + end) + + -- do initial scan after starting the watcher. + -- Scanning first, and then starting the watcher leaves a period of time + -- where a new file may be created (after scanning, before watching), where + -- we wont know about it. + -- If I understand the uv-eventloop correctly, this function, `new`, will + -- be executed completely before a callback is called, so self.files and + -- self.dir_watchers should be populated correctly when a callback is + -- received, even if it was received before all directories/files were + -- added. + -- This difference can be observed, at least on my machine, by watching a + -- directory A, and then creating a nested directory B, and children for it + -- in one command, ie. `mkdir -p A/B/{1,2,3,4,5,6,7,8,9}`. + -- If the callback is registered after the scan, the latter directories + -- (ie. 4-9) did not show up, whereas everything did work correctly if the + -- watcher was activated before the scan. + -- (almost everything, one directory was included in the initial scan and + -- the watch-event, but that seems okay for our purposes) + local files, dirs = Path.scandir(self.root) + for _, file in ipairs(files) do + local relpath = file:sub(#self.root+2) + self:new_file(relpath, file) + end + for _, dir in ipairs(dirs) do + local relpath = dir:sub(#self.root+2) + self:new_dir(relpath, dir) + end +end -- these functions maintain our logical view of the directory, and call -- callbacks when we detect a change. @@ -24,7 +105,7 @@ function TreeWatcher:new_file(rel, full) return end - log.info("new file %s %s", rel, full) + log.debug("new file %s %s", rel, full) self.files[rel] = true self.callbacks.new_file(full) end @@ -34,7 +115,7 @@ function TreeWatcher:new_dir(rel, full) return end - log.info("new dir %s %s", rel, full) + log.debug("new dir %s %s", rel, full) -- first do callback for this directory, then look into (and potentially do -- callbacks for) children. self.callbacks.new_dir(full) @@ -43,17 +124,17 @@ end function TreeWatcher:change_child(rel, full) if self.dir_watchers[rel] then - log.info("changed dir %s %s", rel, full) + log.debug("changed dir %s %s", rel, full) self.callbacks.change_dir(full) elseif self.files[rel] then - log.info("changed file %s %s", rel, full) + log.debug("changed file %s %s", rel, full) self.callbacks.change_file(full) end end function TreeWatcher:remove_child(rel, full) if self.dir_watchers[rel] then - log.info("removing dir %s %s", rel, full) + log.debug("removing dir %s %s", rel, full) -- should have been stopped by the watcher for the child, or it was not -- even started due to depth. self.dir_watchers[rel]:remove_root() @@ -61,7 +142,7 @@ function TreeWatcher:remove_child(rel, full) self.callbacks.remove_dir(full) elseif self.files[rel] then - log.info("removing file %s %s", rel, full) + log.debug("removing file %s %s", rel, full) self.files[rel] = nil self.callbacks.remove_file(full) @@ -73,7 +154,7 @@ function TreeWatcher:remove_root() -- already removed return end - log.info("removing root %s", self.root) + log.debug("removing root %s", self.root) self.removed = true -- stop own, children should have handled themselves, if they are watched. self.fs_event:stop() @@ -95,7 +176,14 @@ end local callback_mt = { __index = function() return util.nop end } -function M.new(root, depth, callbacks) +-- root needs to be an absolute path. +function M.new(root, depth, callbacks, opts) + opts = opts or {} + + -- if lazy is set, watching a non-existing directory will create a watcher + -- for the parent-directory (or its parent, if it does not yet exist). + local lazy = vim.F.if_nil(opts.lazy, false) + -- do nothing on missing callback. callbacks = setmetatable(callbacks or {}, callback_mt) @@ -109,78 +197,30 @@ function M.new(root, depth, callbacks) depth = depth }, TreeWatcher_mt) - -- don't watch children. - if depth == 0 then - return o - end - - -- does not work on nfs-drive, at least if it's edited from another - -- machine. - o.fs_event:start(root, {}, function(err, relpath, events) - if o.removed then - return + -- if the path does not yet exist, set watcher up s.t. it will start + -- watching when the directory is created. + if not uv.fs_stat(root) and lazy then + -- root does not yet exist, need to create a watcher that notifies us + -- of its creation. + local parent_path = Path.parent(root) + if not parent_path then + error(("Could not find parent-path for %s"):format(root)) end - vim.schedule_wrap(function() - log.debug("raw: root: %s; err: %s; relpath: %s; change: %s; rename: %s", o.root, err, relpath, events.change, events.rename) - local full_path = Path.join(root, relpath) - local path_stat = uv.fs_stat(full_path) - - -- try to figure out what happened in the directory. - if events.rename then - if not uv.fs_stat(root) then - o:remove_root() - return - end - if not path_stat then - o:remove_child(relpath, full_path) - return - end - - local f_type - if path_stat.type == "link" then - f_type = uv.fs_stat(uv.fs_realpath(full_path)) - else - f_type = path_stat.type - end - if f_type == "file" then - o:new_file(relpath, full_path) - return - elseif f_type == "directory" then - o:new_dir(relpath, full_path) - return - end - elseif events.change then - o:change_child(relpath, full_path) - end - end)() - end) - - -- do initial scan after starting the watcher. - -- Scanning first, and then starting the watcher leaves a period of time - -- where a new file may be created (after scanning, before watching), where - -- we wont know about it. - -- If I understand the uv-eventloop correctly, this function, `new`, will - -- be executed completely before a callback is called, so o.files and - -- o.dir_watchers should be populated correctly when a callback is - -- received, even if it was received before all directories/files were - -- added. - -- This difference can be observed, at least on my machine, by watching a - -- directory A, and then creating a nested directory B, and children for it - -- in one command, ie. `mkdir -p A/B/{1,2,3,4,5,6,7,8,9}`. - -- If the callback is registered after the scan, the latter directories - -- (ie. 4-9) did not show up, whereas everything did work correctly if the - -- watcher was activated before the scan. - -- (almost everything, one directory was included in the initial scan and - -- the watch-event, but that seems okay for our purposes) - local files, dirs = Path.scandir(root) - for _, file in ipairs(files) do - local relpath = file:sub(#root+2) - o:new_file(relpath, file) - end - for _, dir in ipairs(dirs) do - local relpath = dir:sub(#root+2) - o:new_dir(relpath, dir) + log.info("Path %s does not exist yet, watching %s for creation.", root, parent_path) + + local parent_watcher + parent_watcher = M.new(parent_path, 1, { + new_dir = function(full) + if full == root then + o:start() + -- directory was created, stop watching. + parent_watcher:stop() + end + end, + }, { lazy = true }) + else + o:start() end return o diff --git a/lua/luasnip/loaders/util.lua b/lua/luasnip/loaders/util.lua index 2dcf9cfad..9c44108d7 100644 --- a/lua/luasnip/loaders/util.lua +++ b/lua/luasnip/loaders/util.lua @@ -57,6 +57,18 @@ local function resolve_root_paths(paths, rtp_dirname) return paths end +local function resolve_lazy_root_paths(paths) + if type(paths) == "string" then + paths = vim.split(paths, ",") + end + + paths = vim.tbl_map(Path.expand_nonexisting, paths) + paths = vim.tbl_filter(_is_present, paths) + paths = util.deduplicate(paths) + + return paths +end + local function ft_filter(exclude, include) exclude = filetypelist_to_set(exclude) include = filetypelist_to_set(include) @@ -246,6 +258,7 @@ return { filetypelist_to_set = filetypelist_to_set, split_lines = split_lines, resolve_root_paths = resolve_root_paths, + resolve_lazy_root_paths = resolve_lazy_root_paths, ft_filter = ft_filter, get_ft_paths = get_ft_paths, get_load_paths_snipmate_like = get_load_paths_snipmate_like,