diff --git a/lua/neo-tree/defaults.lua b/lua/neo-tree/defaults.lua index c11ab195..94356e76 100644 --- a/lua/neo-tree/defaults.lua +++ b/lua/neo-tree/defaults.lua @@ -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 = "*", @@ -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 diff --git a/lua/neo-tree/sources/common/components.lua b/lua/neo-tree/sources/common/components.lua index d6a53f00..fb9a2ab2 100644 --- a/lua/neo-tree/sources/common/components.lua +++ b/lua/neo-tree/sources/common/components.lua @@ -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 diff --git a/lua/neo-tree/sources/filesystem/init.lua b/lua/neo-tree/sources/filesystem/init.lua index 0dba706b..6863c9d5 100644 --- a/lua/neo-tree/sources/filesystem/init.lua +++ b/lua/neo-tree/sources/filesystem/init.lua @@ -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 diff --git a/lua/neo-tree/sources/filesystem/lib/fs_scan.lua b/lua/neo-tree/sources/filesystem/lib/fs_scan.lua index bcee6df8..b81951b5 100644 --- a/lua/neo-tree/sources/filesystem/lib/fs_scan.lua +++ b/lua/neo-tree/sources/filesystem/lib/fs_scan.lua @@ -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 @@ -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 @@ -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 diff --git a/lua/neo-tree/ui/renderer.lua b/lua/neo-tree/ui/renderer.lua index bf150f4c..5e9106b7 100644 --- a/lua/neo-tree/ui/renderer.lua +++ b/lua/neo-tree/ui/renderer.lua @@ -1115,30 +1115,37 @@ M.show_nodes = function(sourceItems, state, parentId, callback) if state.group_empty_dirs then if parent then - -- this is a lazy load of a single sub folder - group_empty_dirs(sourceItems) - if #sourceItems == 1 and sourceItems[1].type == "directory" then - -- This folder needs to be grouped. - -- The goal is to just update the existing node in place. - -- To avoid digging into private internals of Nui, we will just export the entire level and replace - -- the one node. This keeps it in the right order, because nui doesn't have methods to replace something - -- in place. - -- We can't just mutate the existing node because we have to change it's id which would break Nui's - -- internal state. - local item = sourceItems[1] - parentId = parent:get_parent_id() - local siblings = state.tree:get_nodes(parentId) - for i, node in pairs(siblings) do - if node.id == parent.id then - item.name = parent.name .. utils.path_separator .. item.name - item.level = level - 1 - item.is_loaded = utils.truthy(item.children) - siblings[i] = NuiTree.Node(item, item.children) - break + local scan_mode = require("neo-tree").config.filesystem.scan_mode + if scan_mode == "deep" then + for i, item in ipairs(sourceItems) do + sourceItems[i] = group_empty_dirs(item) + end + else + -- this is a lazy load of a single sub folder + group_empty_dirs(sourceItems) + if #sourceItems == 1 and sourceItems[1].type == "directory" then + -- This folder needs to be grouped. + -- The goal is to just update the existing node in place. + -- To avoid digging into private internals of Nui, we will just export the entire level and replace + -- the one node. This keeps it in the right order, because nui doesn't have methods to replace something + -- in place. + -- We can't just mutate the existing node because we have to change it's id which would break Nui's + -- internal state. + local item = sourceItems[1] + parentId = parent:get_parent_id() + local siblings = state.tree:get_nodes(parentId) + for i, node in pairs(siblings) do + if node.id == parent.id then + item.name = parent.name .. utils.path_separator .. item.name + item.level = level - 1 + item.is_loaded = utils.truthy(item.children) + siblings[i] = NuiTree.Node(item, item.children) + break + end end + sourceItems = nil -- this is a signal to skip the rest of the processing + state.tree:set_nodes(siblings, parentId) end - sourceItems = nil -- this is a signal to skip the rest of the processing - state.tree:set_nodes(siblings, parentId) end else -- if we are rendering a whole tree, just group the children because we don'the