Skip to content

Commit

Permalink
lua-loader: support loading paths that don't yet exist.
Browse files Browse the repository at this point in the history
Seems very useful for project-local snippets, where a general load
cwd/.luasnippets as a lazy_path would be enough to get them picked up
immediately after creation.
Downside is the additional overhead from watching the parent directory
(or ies, if multiple don't exist).
  • Loading branch information
L3MON4D3 committed Oct 9, 2023
1 parent 54f458f commit 410ed54
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 85 deletions.
29 changes: 23 additions & 6 deletions lua/luasnip/loaders/from_lua.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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

Expand Down
198 changes: 119 additions & 79 deletions lua/luasnip/loaders/tree_watcher.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -43,25 +124,25 @@ 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()
self.dir_watchers[rel] = nil

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)
Expand All @@ -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()
Expand All @@ -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)

Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions lua/luasnip/loaders/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 410ed54

Please sign in to comment.