Skip to content

feat: New node-scanning algorithm and empty folder states #600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 12, 2023
3 changes: 3 additions & 0 deletions lua/neo-tree/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ local config = {
folder_closed = "",
folder_open = "",
folder_empty = "ﰊ",
folder_empty_open = "ﰊ",
-- The next two settings are only a fallback, if you use nvim-web-devicons and configure default icons there
-- then these will never be used.
default = "*",
Expand Down Expand Up @@ -365,6 +366,8 @@ local config = {
async_directory_scan = "auto", -- "auto" means refreshes are async, but it's synchronous when called from the Neotree commands.
-- "always" means directory scans are always async.
-- "never" means directory scans are never async.
scan_mode = "shallow", -- "shallow": Don't scan into directories to detect possible empty directory a priori
-- "deep": Scan into directories to detect empty or grouped empty directories a priori.
bind_to_cwd = true, -- true creates a 2-way binding between vim's cwd and neo-tree's root
cwd_target = {
sidebar = "tab", -- sidebar is when position = left or right
Expand Down
6 changes: 5 additions & 1 deletion lua/neo-tree/sources/common/components.lua
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,17 @@ M.filtered_by = function(config, node, state)
return result
end



M.icon = function(config, node, state)
local icon = config.default or " "
local highlight = config.highlight or highlights.FILE_ICON
if node.type == "directory" then
highlight = highlights.DIRECTORY_ICON
if node.loaded and not node:has_children() then
icon = config.folder_empty or config.folder_open or "-"
icon = not node.empty_expanded
and config.folder_empty
or config.folder_empty_open
elseif node:is_expanded() then
icon = config.folder_open or "-"
else
Expand Down
3 changes: 3 additions & 0 deletions lua/neo-tree/sources/filesystem/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursiv
if path_to_reveal then
renderer.focus_node(state, path_to_reveal)
end
elseif require("neo-tree").config.filesystem.scan_mode == "deep" then
node.empty_expanded = not node.empty_expanded
renderer.redraw(state)
end
end

Expand Down
268 changes: 187 additions & 81 deletions lua/neo-tree/sources/filesystem/lib/fs_scan.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local log = require("neo-tree.log")
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
local git = require("neo-tree.git")
local events = require("neo-tree.events")
local async = require("plenary.async")

local Path = require("plenary.path")
local os_sep = Path.path.sep
Expand Down Expand Up @@ -138,114 +139,218 @@ local job_complete = function(context)
render_context(context)
end


local function create_node(context, node)
local success3, item = pcall(file_items.create_item, context, node.path, node.type)
end

local function process_node(context, path)
on_directory_loaded(context, path)
end

local function get_children_sync(path)
local children = {}
local success, dir = pcall(vim.loop.fs_opendir, path, nil, 1000)
if not success then
log.error("Error opening dir:", dir)
end
local success2, stats = pcall(vim.loop.fs_readdir, dir)
if success2 and stats then
for _, stat in ipairs(stats) do
local child_path = utils.path_join(path, stat.name)
table.insert(children, { path = child_path, type = stat.type })
end
end
vim.loop.fs_closedir(dir)
return children
end

local function get_children_async(path, callback)
uv.fs_opendir(path, function(_, dir)
uv.fs_readdir(dir, function(_, stats)
local children = {}
if stats then
for _, stat in ipairs(stats) do
local child_path = utils.path_join(path, stat.name)
table.insert(children, { path = child_path, type = stat.type })
end
end
uv.fs_closedir(dir)
callback(children)
end)
end, 1000)
end

local function scan_dir_sync(context, path)
process_node(context, path)
local children = get_children_sync(path)
for _, child in ipairs(children) do
create_node(context, child)
if child.type == "directory" then
local grandchild_nodes = get_children_sync(child.path)
if
grandchild_nodes == nil
or #grandchild_nodes == 0
or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory"
then
scan_dir_sync(context, child.path)
end
end
end
end

local function scan_dir_async(context, path, callback)
get_children_async(path, function(children)
for _, child in ipairs(children) do
create_node(context, child)
if child.type == "directory" then
local grandchild_nodes = get_children_sync(child.path)
if
grandchild_nodes == nil
or #grandchild_nodes == 0
or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory"
then
scan_dir_sync(context, child.path)
end
end
end
process_node(context, path)
callback(path)
end)
end

-- async_scan scans all the directories in context.paths_to_load
-- and adds them as items to render in the UI.
local function async_scan(context, path)
log.trace("async_scan: ", path)
-- prepend the root path
table.insert(context.paths_to_load, 1, path)
local scan_mode = require("neo-tree").config.filesystem.scan_mode
if scan_mode == "deep" then
local scan_tasks = {}
for _, p in ipairs(context.paths_to_load) do
local scan_task = async.wrap(function(callback)
scan_dir_async(context, p, callback)
end, 1)
table.insert(scan_tasks, scan_task)
end

context.directories_scanned = 0
context.directories_to_scan = #context.paths_to_load
async.util.run_all(
scan_tasks,
vim.schedule_wrap(function()
job_complete(context)
end)
)
else -- scan_mode == "shallow"
context.directories_scanned = 0
context.directories_to_scan = #context.paths_to_load

context.on_exit = vim.schedule_wrap(function()
job_complete(context)
end)
context.on_exit = vim.schedule_wrap(function()
job_complete(context)
end)

-- from https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/scandir.lua
local function read_dir(current_dir, ctx)
local function on_fs_opendir(err, dir)
if err then
log.error(current_dir, ": ", err)
else
local function on_fs_readdir(err, entries)
if err then
log.error(current_dir, ": ", err)
else
if entries then
for _, entry in ipairs(entries) do
local success, item = pcall(
file_items.create_item,
ctx,
utils.path_join(current_dir, entry.name),
entry.type
)
if success then
if ctx.recursive and item.type == "directory" then
ctx.directories_to_scan = ctx.directories_to_scan + 1
table.insert(ctx.paths_to_load, item.path)
-- from https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/scandir.lua
local function read_dir(current_dir, ctx)
local function on_fs_opendir(err, dir)
if err then
log.error(current_dir, ": ", err)
else
local function on_fs_readdir(err, entries)
if err then
log.error(current_dir, ": ", err)
else
if entries then
for _, entry in ipairs(entries) do
local success, item = pcall(
file_items.create_item,
ctx,
utils.path_join(current_dir, entry.name),
entry.type
)
if success then
if ctx.recursive and item.type == "directory" then
ctx.directories_to_scan = ctx.directories_to_scan + 1
table.insert(ctx.paths_to_load, item.path)
end
else
log.error("error creating item for ", path)
end
else
log.error("error creating item for ", path)
end
end

uv.fs_readdir(dir, on_fs_readdir)
else
uv.fs_closedir(dir)
on_directory_loaded(ctx, current_dir)
ctx.directories_scanned = ctx.directories_scanned + 1
if ctx.directories_scanned == #ctx.paths_to_load then
ctx.on_exit()
uv.fs_readdir(dir, on_fs_readdir)
else
uv.fs_closedir(dir)
on_directory_loaded(ctx, current_dir)
ctx.directories_scanned = ctx.directories_scanned + 1
if ctx.directories_scanned == #ctx.paths_to_load then
ctx.on_exit()
end
end
end

--local next_path = dir_complete(ctx, current_dir)
--if next_path then
-- local success, error = pcall(read_dir, next_path)
-- if not success then
-- log.error(next_path, ": ", error)
-- end
--else
-- on_exit()
--end
end

--local next_path = dir_complete(ctx, current_dir)
--if next_path then
-- local success, error = pcall(read_dir, next_path)
-- if not success then
-- log.error(next_path, ": ", error)
-- end
--else
-- on_exit()
--end
uv.fs_readdir(dir, on_fs_readdir)
end

uv.fs_readdir(dir, on_fs_readdir)
end
end

uv.fs_opendir(current_dir, on_fs_opendir)
end
uv.fs_opendir(current_dir, on_fs_opendir)
end

--local first = table.remove(context.paths_to_load)
--local success, err = pcall(read_dir, first)
--if not success then
-- log.error(first, ": ", err)
--end
for i = 1, context.directories_to_scan do
read_dir(context.paths_to_load[i], context)
--local first = table.remove(context.paths_to_load)
--local success, err = pcall(read_dir, first)
--if not success then
-- log.error(first, ": ", err)
--end
for i = 1, context.directories_to_scan do
read_dir(context.paths_to_load[i], context)
end
end
end

local function sync_scan(context, path_to_scan)
log.trace("sync_scan: ", path_to_scan)
local success, dir = pcall(vim.loop.fs_opendir, path_to_scan, nil, 1000)
if not success then
log.error("Error opening dir:", dir)
end
local success2, stats = pcall(vim.loop.fs_readdir, dir)
if success2 and stats then
for _, stat in ipairs(stats) do
local path = utils.path_join(path_to_scan, stat.name)
local success3, item = pcall(file_items.create_item, context, path, stat.type)
if success3 then
if context.recursive and stat.type == "directory" then
table.insert(context.paths_to_load, path)
local scan_mode = require("neo-tree").config.filesystem.scan_mode
if scan_mode == "deep" then
for _, path in ipairs(context.paths_to_load) do
scan_dir_sync(context, path)
-- scan_dir(context, path)
end
job_complete(context)
else -- scan_mode == "shallow"
local success, dir = pcall(vim.loop.fs_opendir, path_to_scan, nil, 1000)
if not success then
log.error("Error opening dir:", dir)
end
local success2, stats = pcall(vim.loop.fs_readdir, dir)
if success2 and stats then
for _, stat in ipairs(stats) do
local path = utils.path_join(path_to_scan, stat.name)
local success3, item = pcall(file_items.create_item, context, path, stat.type)
if success3 then
if context.recursive and stat.type == "directory" then
table.insert(context.paths_to_load, path)
end
else
log.error("error creating item for ", path)
end
else
log.error("error creating item for ", path)
end
end
end
vim.loop.fs_closedir(dir)
vim.loop.fs_closedir(dir)

local next_path = dir_complete(context, path_to_scan)
if next_path then
sync_scan(context, next_path)
else
job_complete(context)
local next_path = dir_complete(context, path_to_scan)
if next_path then
sync_scan(context, next_path)
else
job_complete(context)
end
end
end

Expand Down Expand Up @@ -357,6 +462,7 @@ M.get_items = function(state, parent_id, path_to_reveal, callback, async, recurs
end
return false
end
table.insert(context.paths_to_load, path)
if async then
async_scan(context, path)
else
Expand Down
Loading