diff --git a/lua/neo-tree/git/ignored.lua b/lua/neo-tree/git/ignored.lua index c922e6e8..69401aca 100644 --- a/lua/neo-tree/git/ignored.lua +++ b/lua/neo-tree/git/ignored.lua @@ -118,7 +118,7 @@ M.mark_ignored = function(state, items, callback) on_exit = function(self, code, _) local result if code ~= 0 then - log.debug("Failed to load ignored files for", state.path, ":", self:stderr_result()) + log.debug("Failed to load ignored files for", folder, ":", self:stderr_result()) result = {} else result = self:result() diff --git a/lua/neo-tree/sources/common/commands.lua b/lua/neo-tree/sources/common/commands.lua index cd9ce102..f5dfc561 100644 --- a/lua/neo-tree/sources/common/commands.lua +++ b/lua/neo-tree/sources/common/commands.lua @@ -10,6 +10,8 @@ local popups = require("neo-tree.ui.popups") local log = require("neo-tree.log") local help = require("neo-tree.sources.common.help") local Preview = require("neo-tree.sources.common.preview") +local async = require("plenary.async") +local node_expander = require("neo-tree.sources.common.node_expander") ---Gets the node parent folder ---@param state table to look for nodes @@ -108,34 +110,28 @@ M.add_directory = function(state, callback) fs_actions.create_directory(in_directory, callback, using_root_directory) end -M.expand_all_nodes = function(state, toggle_directory) - if toggle_directory == nil then - toggle_directory = function(_, node) - node:expand() - end +---Expand all nodes +---@param state table The state of the source +---@param node table A node to expand +---@param prefetcher table an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean` +M.expand_all_nodes = function(state, node, prefetcher) + log.debug("Expanding all nodes under " .. node:get_id()) + if prefetcher == nil then + prefetcher = node_expander.default_prefetcher end - local expand_node - expand_node = function(node) - local id = node:get_id() - if node.type == "directory" and not node:is_expanded() then - toggle_directory(state, node) - node = state.tree:get_node(id) - end - local children = state.tree:get_nodes(id) - if children then - for _, child in ipairs(children) do - if child.type == "directory" then - expand_node(child) - end - end - end - end + renderer.position.set(state, nil) - for _, node in ipairs(state.tree:get_nodes()) do - expand_node(node) + local task = function () + node_expander.expand_directory_recursively(state, node, prefetcher) end - renderer.redraw(state) + async.run( + task, + function () + log.debug("All nodes expanded - redrawing") + renderer.redraw(state) + end + ) end M.close_node = function(state, callback) diff --git a/lua/neo-tree/sources/common/node_expander.lua b/lua/neo-tree/sources/common/node_expander.lua new file mode 100644 index 00000000..c04f736c --- /dev/null +++ b/lua/neo-tree/sources/common/node_expander.lua @@ -0,0 +1,82 @@ +local log = require("neo-tree.log") + +local M = {} + +--- Recursively expand all loaded nodes under the given node +--- returns table with all discovered nodes that need to be loaded +---@param node table a node to expand +---@param state table current state of the source +---@return table discovered nodes that need to be loaded +local function expand_loaded(node, state, prefetcher) + local function rec(current_node, to_load) + if prefetcher.should_prefetch(current_node) then + log.trace("Node " .. current_node:get_id() .. "not loaded, saving for later") + table.insert(to_load, current_node) + else + if not current_node:is_expanded() then + current_node:expand() + state.explicitly_opened_directories[current_node:get_id()] = true + end + local children = state.tree:get_nodes(current_node:get_id()) + log.debug("Expanding childrens of " .. current_node:get_id()) + for _, child in ipairs(children) do + if child.type == "directory" then + rec(child, to_load) + else + log.trace("Child: " .. child.name .. " is not a directory, skipping") + end + end + end + end + + local to_load = {} + rec(node, to_load) + return to_load +end + +--- Recursively expands all nodes under the given node collecting all unloaded nodes +--- Then run prefetcher on all unloaded nodes. Finally, expand loded nodes. +--- async method +---@param node table a node to expand +---@param state table current state of the source +local function expand_and_load(node, state, prefetcher) + local to_load = expand_loaded(node, state, prefetcher) + for _, _node in ipairs(to_load) do + prefetcher.prefetch(state, _node) + -- no need to handle results as prefetch is recursive + expand_loaded(_node, state, prefetcher) + end +end + +--- Expands given node recursively loading all descendant nodes if needed +--- Nodes will be loaded using given prefetcher +--- async method +---@param state table current state of the source +---@param node table a node to expand +---@param prefetcher table an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean` +M.expand_directory_recursively = function(state, node, prefetcher) + log.debug("Expanding directory " .. node:get_id()) + if node.type ~= "directory" then + return + end + state.explicitly_opened_directories = state.explicitly_opened_directories or {} + if prefetcher.should_prefetch(node) then + local id = node:get_id() + state.explicitly_opened_directories[id] = true + prefetcher.prefetch(state, node) + expand_loaded(node, state, prefetcher) + else + expand_and_load(node, state, prefetcher) + end +end + +M.default_prefetcher = { + prefetch = function (state, node) + log.debug("Default expander prefetch does nothing") + end, + should_prefetch = function (node) + return false + end +} + +return M diff --git a/lua/neo-tree/sources/filesystem/commands.lua b/lua/neo-tree/sources/filesystem/commands.lua index 160ca604..f2d242b2 100644 --- a/lua/neo-tree/sources/filesystem/commands.lua +++ b/lua/neo-tree/sources/filesystem/commands.lua @@ -67,11 +67,11 @@ M.delete_visual = function(state, selected_nodes) cc.delete_visual(state, selected_nodes, utils.wrap(refresh, state)) end -M.expand_all_nodes = function(state) - local toggle_dir_no_redraw = function(_state, node) - fs.toggle_directory(_state, node, nil, true, true) +M.expand_all_nodes = function(state, node) + if node == nil then + node = state.tree:get_node(state.path) end - cc.expand_all_nodes(state, toggle_dir_no_redraw) + cc.expand_all_nodes(state, node, fs.prefetcher) end ---Shows the filter input, which will filter the tree. diff --git a/lua/neo-tree/sources/filesystem/init.lua b/lua/neo-tree/sources/filesystem/init.lua index beb2c0fb..9971512f 100644 --- a/lua/neo-tree/sources/filesystem/init.lua +++ b/lua/neo-tree/sources/filesystem/init.lua @@ -393,7 +393,7 @@ M.setup = function(config, global_config) end ---Expands or collapses the current node. -M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursive) +M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursive, callback) local tree = state.tree if not node then node = tree:get_node() @@ -406,7 +406,7 @@ M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursiv local id = node:get_id() state.explicitly_opened_directories[id] = true renderer.position.set(state, nil) - fs_scan.get_items(state, id, path_to_reveal, nil, false, recursive) + fs_scan.get_items(state, id, path_to_reveal, callback, false, recursive) elseif node:has_children() then local updated = false if node:is_expanded() then @@ -428,4 +428,14 @@ M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursiv end end +M.prefetcher = { + prefetch = function (state, node) + log.debug("Running fs prefetch for: " .. node:get_id()) + fs_scan.get_dir_items_async(state, node:get_id(), true) + end, + should_prefetch = function (node) + return not node.loaded + end +} + return M diff --git a/lua/neo-tree/sources/filesystem/lib/fs_scan.lua b/lua/neo-tree/sources/filesystem/lib/fs_scan.lua index 5bf6e28b..86c12fac 100644 --- a/lua/neo-tree/sources/filesystem/lib/fs_scan.lua +++ b/lua/neo-tree/sources/filesystem/lib/fs_scan.lua @@ -11,9 +11,6 @@ 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 - local M = {} local on_directory_loaded = function(context, dir_path) @@ -106,6 +103,26 @@ local render_context = function(context) context = nil end +local job_complete_async = function(context) + local state = context.state + local parent_id = context.parent_id + if #context.all_items == 0 then + log.info("No items, skipping git ignored/status lookups") + elseif state.filtered_items.hide_gitignored or state.enable_git_status then + local mark_ignored_async = async.wrap(function (_state, _all_items, _callback) + git.mark_ignored(_state, _all_items, _callback) + end, 3) + local all_items = mark_ignored_async(state, context.all_items) + + if parent_id then + vim.list_extend(state.git_ignored, all_items) + else + state.git_ignored = all_items + end + end + return context +end + local job_complete = function(context) local state = context.state local parent_id = context.parent_id @@ -135,8 +152,8 @@ local job_complete = function(context) state.git_ignored = all_items end end + render_context(context) end - render_context(context) end local function create_node(context, node) @@ -190,7 +207,8 @@ local function scan_dir_sync(context, path) if grandchild_nodes == nil or #grandchild_nodes == 0 - or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory" + or (#grandchild_nodes == 1 and grandchild_nodes[1].type == "directory") + or context.recursive then scan_dir_sync(context, child.path) end @@ -198,26 +216,36 @@ local function scan_dir_sync(context, path) 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 +--- async method +local function scan_dir_async(context, path) + log.debug("scan_dir_async - start " .. path) + + local get_children = async.wrap(function (_path, callback) + return get_children_async(_path, callback) + end, 2) + + local children = get_children(path) + for _, child in ipairs(children) do + create_node(context, child) + if child.type == "directory" then + local grandchild_nodes = get_children(child.path) + if + grandchild_nodes == nil + or #grandchild_nodes == 0 + or (#grandchild_nodes == 1 and grandchild_nodes[1].type == "directory") + or context.recursive + then + scan_dir_async(context, child.path) end end - process_node(context, path) - callback(path) - end) + end + + process_node(context, path) + log.debug("scan_dir_async - finish " .. path) + return path 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) @@ -227,9 +255,9 @@ local function async_scan(context, path) 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) + local scan_task = function () + scan_dir_async(context, p) + end table.insert(scan_tasks, scan_task) end @@ -362,35 +390,9 @@ M.get_items_async = function(state, parent_id, path_to_reveal, callback) M.get_items(state, parent_id, path_to_reveal, callback, true) end -M.get_items = function(state, parent_id, path_to_reveal, callback, async, recursive) - if state.async_directory_scan == "always" then - async = true - elseif state.async_directory_scan == "never" then - async = false - elseif type(async) == "nil" then - async = (state.async_directory_scan == "auto") or state.async_directory_scan - end - - if not parent_id then - M.stop_watchers(state) - end - local context = file_items.create_context() - context.state = state - context.parent_id = parent_id - context.path_to_reveal = path_to_reveal - context.recursive = recursive - context.callback = callback - - -- Create root folder - local root = file_items.create_item(context, parent_id or state.path, "directory") - root.name = vim.fn.fnamemodify(root.path, ":~") - root.loaded = true - root.search_pattern = state.search_pattern - context.root = root - context.folders[root.path] = root - state.default_expanded_nodes = state.force_open_folders or { state.path } - - if state.search_pattern then +local handle_search_pattern = function (context) + local state = context.state + local root = context.root local search_opts = { filtered_items = state.filtered_items, find_command = state.find_command, @@ -417,9 +419,12 @@ M.get_items = function(state, parent_id, path_to_reveal, callback, async, recurs -- Use the external command because the plenary search is slow filter_external.find_files(search_opts) end - else - -- In the case of a refresh or navigating up, we need to make sure that all - -- open folders are loaded. +end + +local handle_refresh_or_up = function (context, async) + local parent_id = context.parent_id + local path_to_reveal = context.path_to_reveal + local state = context.state local path = parent_id or state.path context.paths_to_load = {} if parent_id == nil then @@ -473,7 +478,98 @@ M.get_items = function(state, parent_id, path_to_reveal, callback, async, recurs else sync_scan(context, path) end +end + +M.get_items = function(state, parent_id, path_to_reveal, callback, async, recursive) + if state.async_directory_scan == "always" then + async = true + elseif state.async_directory_scan == "never" then + async = false + elseif type(async) == "nil" then + async = (state.async_directory_scan == "auto") or state.async_directory_scan end + + if not parent_id then + M.stop_watchers(state) + end + local context = file_items.create_context() + context.state = state + context.parent_id = parent_id + context.path_to_reveal = path_to_reveal + context.recursive = recursive + context.callback = callback + -- Create root folder + local root = file_items.create_item(context, parent_id or state.path, "directory") + root.name = vim.fn.fnamemodify(root.path, ":~") + root.loaded = true + root.search_pattern = state.search_pattern + context.root = root + context.folders[root.path] = root + state.default_expanded_nodes = state.force_open_folders or { state.path } + + if state.search_pattern then + handle_search_pattern(context) + else + -- In the case of a refresh or navigating up, we need to make sure that all + -- open folders are loaded. + handle_refresh_or_up(context, async) + end +end + +-- async method +M.get_dir_items_async = function(state, parent_id, recursive) + local context = file_items.create_context() + context.state = state + context.parent_id = parent_id + context.path_to_reveal = nil + context.recursive = recursive + context.callback = nil + context.paths_to_load = {} + + -- Create root folder + local root = file_items.create_item(context, parent_id or state.path, "directory") + root.name = vim.fn.fnamemodify(root.path, ":~") + root.loaded = true + root.search_pattern = state.search_pattern + context.root = root + context.folders[root.path] = root + state.default_expanded_nodes = state.force_open_folders or { state.path } + + local filtered_items = state.filtered_items or {} + context.is_a_never_show_file = function(fname) + if fname then + local _, name = utils.split_path(fname) + if name then + if filtered_items.never_show and filtered_items.never_show[name] then + return true + end + if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then + return true + end + end + end + return false + end + table.insert(context.paths_to_load, parent_id) + + local scan_tasks = {} + for _, p in ipairs(context.paths_to_load) do + local scan_task = function () + scan_dir_async(context, p) + end + table.insert(scan_tasks, scan_task) + end + async.util.join(scan_tasks) + + job_complete_async(context) + + local finalize = async.wrap(function (_context, _callback) + vim.schedule(function () + render_context(_context) + _callback() + end) + end, 2) + finalize(context) end M.stop_watchers = function(state) diff --git a/lua/neo-tree/ui/renderer.lua b/lua/neo-tree/ui/renderer.lua index 34d979e4..5444faf3 100644 --- a/lua/neo-tree/ui/renderer.lua +++ b/lua/neo-tree/ui/renderer.lua @@ -1263,7 +1263,7 @@ M.show_nodes = function(sourceItems, state, parentId, callback) 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) + item.loaded = utils.truthy(item.children) siblings[i] = NuiTree.Node(item, item.children) break end