diff --git a/lua/nvim-tree/actions/finders/find-file.lua b/lua/nvim-tree/actions/finders/find-file.lua index cfc51c0dd19..fcfd24941ff 100644 --- a/lua/nvim-tree/actions/finders/find-file.lua +++ b/lua/nvim-tree/actions/finders/find-file.lua @@ -2,7 +2,6 @@ local log = require "nvim-tree.log" local view = require "nvim-tree.view" local utils = require "nvim-tree.utils" local renderer = require "nvim-tree.renderer" -local reload = require "nvim-tree.explorer.reload" local core = require "nvim-tree.core" local Iterator = require "nvim-tree.iterators.node-iterator" @@ -13,7 +12,8 @@ local running = {} ---Find a path in the tree, expand it and focus it ---@param path string relative or absolute function M.fn(path) - if not core.get_explorer() or not view.is_visible() then + local explorer = core.get_explorer() + if not explorer or not view.is_visible() then return end @@ -32,7 +32,7 @@ function M.fn(path) -- refresh the contents of all parents, expanding groups as needed if utils.get_node_from_path(path_real) == nil then - reload.refresh_parent_nodes_for_path(vim.fn.fnamemodify(path_real, ":h")) + explorer:refresh_parent_nodes_for_path(vim.fn.fnamemodify(path_real, ":h")) end local line = core.get_nodes_starting_line() diff --git a/lua/nvim-tree/actions/reloaders.lua b/lua/nvim-tree/actions/reloaders.lua index 626b280db7f..02126b515d3 100644 --- a/lua/nvim-tree/actions/reloaders.lua +++ b/lua/nvim-tree/actions/reloaders.lua @@ -1,21 +1,22 @@ local git = require "nvim-tree.git" local view = require "nvim-tree.view" local renderer = require "nvim-tree.renderer" -local explorer_module = require "nvim-tree.explorer" local core = require "nvim-tree.core" local explorer_node = require "nvim-tree.explorer.node" local Iterator = require "nvim-tree.iterators.node-iterator" local M = {} ----@param node Explorer|nil +---@param explorer Explorer|nil ---@param projects table -local function refresh_nodes(node, projects) - Iterator.builder({ node }) +local function refresh_nodes(explorer, projects) + Iterator.builder({ explorer }) :applier(function(n) if n.nodes then local toplevel = git.get_toplevel(n.cwd or n.link_to or n.absolute_path) - explorer_module.reload(n, projects[toplevel] or {}) + if explorer then + explorer:reload(n, projects[toplevel] or {}) + end end end) :recursor(function(n) diff --git a/lua/nvim-tree/core.lua b/lua/nvim-tree/core.lua index 50072d40bb5..a255233dab3 100644 --- a/lua/nvim-tree/core.lua +++ b/lua/nvim-tree/core.lua @@ -16,7 +16,7 @@ function M.init(foldername) if TreeExplorer then TreeExplorer:destroy() end - TreeExplorer = explorer.Explorer.new(foldername) + TreeExplorer = explorer:new(foldername) if not first_init_done then events._dispatch_ready() first_init_done = true diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua index 89b02e96e68..f6cdbcaaaa0 100644 --- a/lua/nvim-tree/explorer/init.lua +++ b/lua/nvim-tree/explorer/init.lua @@ -1,17 +1,23 @@ +local builders = require "nvim-tree.explorer.node-builders" local git = require "nvim-tree.git" +local log = require "nvim-tree.log" local notify = require "nvim-tree.notify" +local utils = require "nvim-tree.utils" local watch = require "nvim-tree.explorer.watch" local explorer_node = require "nvim-tree.explorer.node" + +local NodeIterator = require "nvim-tree.iterators.node-iterator" +local Watcher = require "nvim-tree.watcher" + local Filters = require "nvim-tree.explorer.filters" local Marks = {} -- circular dependencies local LiveFilter = require "nvim-tree.explorer.live-filter" local Sorters = require "nvim-tree.explorer.sorters" local Clipboard = {} -- circular dependencies -local M = {} +local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON -M.explore = require("nvim-tree.explorer.explore").explore -M.reload = require("nvim-tree.explorer.reload").reload +local config ---@class Explorer ---@field absolute_path string @@ -22,13 +28,13 @@ M.reload = require("nvim-tree.explorer.reload").reload ---@field sorters Sorter ---@field marks Marks ---@field clipboard Clipboard - local Explorer = {} -Explorer.__index = Explorer + +Explorer.explore = require("nvim-tree.explorer.explore").explore ---@param path string|nil ---@return Explorer|nil -function Explorer.new(path) +function Explorer:new(path) local err if path then @@ -42,27 +48,24 @@ function Explorer.new(path) end ---@class Explorer - local explorer = setmetatable({ + local o = setmetatable({ absolute_path = path, nodes = {}, open = true, - sorters = Sorters:new(M.config), + sorters = Sorters:new(config), }, Explorer) - explorer.watcher = watch.create_watcher(explorer) - explorer.filters = Filters:new(M.config, explorer) - explorer.live_filter = LiveFilter:new(M.config, explorer) - explorer.marks = Marks:new(M.config, explorer) - explorer.clipboard = Clipboard:new(M.config, explorer) - explorer:_load(explorer) - return explorer -end + setmetatable(o, self) + self.__index = self ----@private ----@param node Node -function Explorer:_load(node) - local cwd = node.link_to or node.absolute_path - local git_status = git.load_project_status(cwd) - M.explore(node, git_status, self) + o.watcher = watch.create_watcher(o) + o.filters = Filters:new(config, o) + o.live_filter = LiveFilter:new(config, o) + o.marks = Marks:new(config, o) + o.clipboard = Clipboard:new(config, o) + + o:_load(o) + + return o end ---@param node Node @@ -82,17 +85,253 @@ function Explorer:destroy() iterate(self) end -function M.setup(opts) - M.config = opts +---@param node Node +---@param git_status table|nil +function Explorer:reload(node, git_status) + local cwd = node.link_to or node.absolute_path + local handle = vim.loop.fs_scandir(cwd) + if not handle then + return + end + + local profile = log.profile_start("reload %s", node.absolute_path) + + local filter_status = self.filters:prepare(git_status) + + if node.group_next then + node.nodes = { node.group_next } + node.group_next = nil + end + + local remain_childs = {} + + local node_ignored = explorer_node.is_git_ignored(node) + ---@type table + local nodes_by_path = utils.key_by(node.nodes, "absolute_path") + + -- To reset we must 'zero' everything that we use + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }) + + while true do + local name, t = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + local abs = utils.path_join { cwd, name } + ---@type uv.fs_stat.result|nil + local stat = vim.loop.fs_stat(abs) + + local filter_reason = self.filters:should_filter_as_reason(abs, stat, filter_status) + if filter_reason == FILTER_REASON.none then + remain_childs[abs] = true + + -- Recreate node if type changes. + if nodes_by_path[abs] then + local n = nodes_by_path[abs] + + if n.type ~= t then + utils.array_remove(node.nodes, n) + explorer_node.node_destroy(n) + nodes_by_path[abs] = nil + end + end + + if not nodes_by_path[abs] then + local new_child = nil + if t == "directory" and vim.loop.fs_access(abs, "R") and Watcher.is_fs_event_capable(abs) then + new_child = builders.folder(node, abs, name, stat) + elseif t == "file" then + new_child = builders.file(node, abs, name, stat) + elseif t == "link" then + local link = builders.link(node, abs, name, stat) + if link.link_to ~= nil then + new_child = link + end + end + if new_child then + table.insert(node.nodes, new_child) + nodes_by_path[abs] = new_child + end + else + local n = nodes_by_path[abs] + if n then + n.executable = builders.is_executable(abs) or false + n.fs_stat = stat + end + end + else + for reason, value in pairs(FILTER_REASON) do + if filter_reason == value then + node.hidden_stats[reason] = node.hidden_stats[reason] + 1 + end + end + end + end + + node.nodes = vim.tbl_map( + self:update_status(nodes_by_path, node_ignored, git_status), + vim.tbl_filter(function(n) + if remain_childs[n.absolute_path] then + return remain_childs[n.absolute_path] + else + explorer_node.node_destroy(n) + return false + end + end, node.nodes) + ) + + local is_root = not node.parent + local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] + if config.group_empty and not is_root and child_folder_only then + node.group_next = child_folder_only + local ns = self:reload(child_folder_only, git_status) + node.nodes = ns or {} + log.profile_end(profile) + return ns + end + + self.sorters:sort(node.nodes) + self.live_filter:apply_filter(node) + log.profile_end(profile) + return node.nodes +end + +---TODO #2837 #2871 move this and similar to node +---Refresh contents and git status for a single node +---@param node Node +---@param callback function +function Explorer:refresh_node(node, callback) + if type(node) ~= "table" then + callback() + end + + local parent_node = utils.get_parent_of_group(node) + + self:reload_and_get_git_project(node.absolute_path, function(toplevel, project) + self:reload(parent_node, project) + + self:update_parent_statuses(parent_node, project, toplevel) + + callback() + end) +end + +---Refresh contents of all nodes to a path: actual directory and links. +---Groups will be expanded if needed. +---@param path string absolute path +function Explorer:refresh_parent_nodes_for_path(path) + local profile = log.profile_start("refresh_parent_nodes_for_path %s", path) + + -- collect parent nodes from the top down + local parent_nodes = {} + NodeIterator.builder({ self }) + :recursor(function(node) + return node.nodes + end) + :applier(function(node) + local abs_contains = node.absolute_path and path:find(node.absolute_path, 1, true) == 1 + local link_contains = node.link_to and path:find(node.link_to, 1, true) == 1 + if abs_contains or link_contains then + table.insert(parent_nodes, node) + end + end) + :iterate() + + -- refresh in order; this will expand groups as needed + for _, node in ipairs(parent_nodes) do + local toplevel = git.get_toplevel(node.absolute_path) + local project = git.get_project(toplevel) or {} + + self:reload(node, project) + self:update_parent_statuses(node, project, toplevel) + end + + log.profile_end(profile) +end + +---@private +---@param node Node +function Explorer:_load(node) + local cwd = node.link_to or node.absolute_path + local git_status = git.load_project_status(cwd) + Explorer.explore(node, git_status, self) +end + +function Explorer.setup(opts) + config = opts require("nvim-tree.explorer.node").setup(opts) require("nvim-tree.explorer.explore").setup(opts) - require("nvim-tree.explorer.reload").setup(opts) require("nvim-tree.explorer.watch").setup(opts) Marks = require "nvim-tree.marks" Clipboard = require "nvim-tree.actions.fs.clipboard" end -M.Explorer = Explorer +---@private +---@param nodes_by_path table +---@param node_ignored boolean +---@param status table|nil +---@return fun(node: Node): table +function Explorer:update_status(nodes_by_path, node_ignored, status) + return function(node) + if nodes_by_path[node.absolute_path] then + explorer_node.update_git_status(node, node_ignored, status) + end + return node + end +end + +---TODO #2837 #2871 move this and similar to node +---@private +---@param path string +---@param callback fun(toplevel: string|nil, project: table|nil) +function Explorer:reload_and_get_git_project(path, callback) + local toplevel = git.get_toplevel(path) + + git.reload_project(toplevel, path, function() + callback(toplevel, git.get_project(toplevel) or {}) + end) +end + +---TODO #2837 #2871 move this and similar to node +---@private +---@param node Node +---@param project table|nil +---@param root string|nil +function Explorer:update_parent_statuses(node, project, root) + while project and node do + -- step up to the containing project + if node.absolute_path == root then + -- stop at the top of the tree + if not node.parent then + break + end + + root = git.get_toplevel(node.parent.absolute_path) + + -- stop when no more projects + if not root then + break + end + + -- update the containing project + project = git.get_project(root) + git.reload_project(root, node.absolute_path, nil) + end + + -- update status + explorer_node.update_git_status(node, explorer_node.is_git_ignored(node.parent), project) + + -- maybe parent + node = node.parent + end +end -return M +return Explorer diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua deleted file mode 100644 index ff0f75990aa..00000000000 --- a/lua/nvim-tree/explorer/reload.lua +++ /dev/null @@ -1,251 +0,0 @@ -local utils = require "nvim-tree.utils" -local builders = require "nvim-tree.explorer.node-builders" -local explorer_node = require "nvim-tree.explorer.node" -local git = require "nvim-tree.git" -local log = require "nvim-tree.log" - -local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON -local NodeIterator = require "nvim-tree.iterators.node-iterator" -local Watcher = require "nvim-tree.watcher" - -local M = {} - ----@param nodes_by_path table ----@param node_ignored boolean ----@param status table ----@return fun(node: Node): table -local function update_status(nodes_by_path, node_ignored, status) - return function(node) - if nodes_by_path[node.absolute_path] then - explorer_node.update_git_status(node, node_ignored, status) - end - return node - end -end - ----@param path string ----@param callback fun(toplevel: string|nil, project: table|nil) -local function reload_and_get_git_project(path, callback) - local toplevel = git.get_toplevel(path) - - git.reload_project(toplevel, path, function() - callback(toplevel, git.get_project(toplevel) or {}) - end) -end - ----@param node Node ----@param project table|nil ----@param root string|nil -local function update_parent_statuses(node, project, root) - while project and node do - -- step up to the containing project - if node.absolute_path == root then - -- stop at the top of the tree - if not node.parent then - break - end - - root = git.get_toplevel(node.parent.absolute_path) - - -- stop when no more projects - if not root then - break - end - - -- update the containing project - project = git.get_project(root) - git.reload_project(root, node.absolute_path, nil) - end - - -- update status - explorer_node.update_git_status(node, explorer_node.is_git_ignored(node.parent), project) - - -- maybe parent - node = node.parent - end -end - ----@param node Node ----@param git_status table -function M.reload(node, git_status) - local explorer = require("nvim-tree.core").get_explorer() - if not explorer then - return - end - local cwd = node.link_to or node.absolute_path - local handle = vim.loop.fs_scandir(cwd) - if not handle then - return - end - - local profile = log.profile_start("reload %s", node.absolute_path) - - local filter_status = explorer.filters:prepare(git_status) - - if node.group_next then - node.nodes = { node.group_next } - node.group_next = nil - end - - local remain_childs = {} - - local node_ignored = explorer_node.is_git_ignored(node) - ---@type table - local nodes_by_path = utils.key_by(node.nodes, "absolute_path") - - -- To reset we must 'zero' everything that we use - node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { - git = 0, - buf = 0, - dotfile = 0, - custom = 0, - bookmark = 0, - }) - - while true do - local name, t = vim.loop.fs_scandir_next(handle) - if not name then - break - end - - local abs = utils.path_join { cwd, name } - ---@type uv.fs_stat.result|nil - local stat = vim.loop.fs_stat(abs) - - local filter_reason = explorer.filters:should_filter_as_reason(abs, stat, filter_status) - if filter_reason == FILTER_REASON.none then - remain_childs[abs] = true - - -- Recreate node if type changes. - if nodes_by_path[abs] then - local n = nodes_by_path[abs] - - if n.type ~= t then - utils.array_remove(node.nodes, n) - explorer_node.node_destroy(n) - nodes_by_path[abs] = nil - end - end - - if not nodes_by_path[abs] then - local new_child = nil - if t == "directory" and vim.loop.fs_access(abs, "R") and Watcher.is_fs_event_capable(abs) then - new_child = builders.folder(node, abs, name, stat) - elseif t == "file" then - new_child = builders.file(node, abs, name, stat) - elseif t == "link" then - local link = builders.link(node, abs, name, stat) - if link.link_to ~= nil then - new_child = link - end - end - if new_child then - table.insert(node.nodes, new_child) - nodes_by_path[abs] = new_child - end - else - local n = nodes_by_path[abs] - if n then - n.executable = builders.is_executable(abs) or false - n.fs_stat = stat - end - end - else - for reason, value in pairs(FILTER_REASON) do - if filter_reason == value then - node.hidden_stats[reason] = node.hidden_stats[reason] + 1 - end - end - end - end - - node.nodes = vim.tbl_map( - update_status(nodes_by_path, node_ignored, git_status), - vim.tbl_filter(function(n) - if remain_childs[n.absolute_path] then - return remain_childs[n.absolute_path] - else - explorer_node.node_destroy(n) - return false - end - end, node.nodes) - ) - - local is_root = not node.parent - local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] - if M.config.group_empty and not is_root and child_folder_only then - node.group_next = child_folder_only - local ns = M.reload(child_folder_only, git_status) - node.nodes = ns or {} - log.profile_end(profile) - return ns - end - - explorer.sorters:sort(node.nodes) - explorer.live_filter:apply_filter(node) - log.profile_end(profile) - return node.nodes -end - ----Refresh contents and git status for a single node ----@param node Node ----@param callback function -function M.refresh_node(node, callback) - if type(node) ~= "table" then - callback() - end - - local parent_node = utils.get_parent_of_group(node) - - reload_and_get_git_project(node.absolute_path, function(toplevel, project) - require("nvim-tree.explorer.reload").reload(parent_node, project) - - update_parent_statuses(parent_node, project, toplevel) - - callback() - end) -end - ----Refresh contents of all nodes to a path: actual directory and links. ----Groups will be expanded if needed. ----@param path string absolute path -function M.refresh_parent_nodes_for_path(path) - local explorer = require("nvim-tree.core").get_explorer() - if not explorer then - return - end - - local profile = log.profile_start("refresh_parent_nodes_for_path %s", path) - - -- collect parent nodes from the top down - local parent_nodes = {} - NodeIterator.builder({ explorer }) - :recursor(function(node) - return node.nodes - end) - :applier(function(node) - local abs_contains = node.absolute_path and path:find(node.absolute_path, 1, true) == 1 - local link_contains = node.link_to and path:find(node.link_to, 1, true) == 1 - if abs_contains or link_contains then - table.insert(parent_nodes, node) - end - end) - :iterate() - - -- refresh in order; this will expand groups as needed - for _, node in ipairs(parent_nodes) do - local toplevel = git.get_toplevel(node.absolute_path) - local project = git.get_project(toplevel) or {} - - M.reload(node, project) - update_parent_statuses(node, project, toplevel) - end - - log.profile_end(profile) -end - -function M.setup(opts) - M.config = opts.renderer -end - -return M diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua index 4995d31ebe9..90ccedb20de 100644 --- a/lua/nvim-tree/explorer/watch.lua +++ b/lua/nvim-tree/explorer/watch.lua @@ -76,9 +76,12 @@ function M.create_watcher(node) else log.line("watcher", "node event executing refresh '%s'", node.absolute_path) end - require("nvim-tree.explorer.reload").refresh_node(node, function() - require("nvim-tree.renderer").draw() - end) + local explorer = require("nvim-tree.core").get_explorer() + if explorer then + explorer:refresh_node(node, function() + require("nvim-tree.renderer").draw() + end) + end end) end