diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7885547..feac234f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: strategy: matrix: nvim_version: [ stable, nightly ] - luals_version: [ 3.11.0 ] + luals_version: [ 3.10.5 ] steps: - uses: actions/checkout@v4 diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 16f2fa33..c65b5143 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -125,7 +125,7 @@ function M.place_cursor_on_node() if not node or node.name == ".." then return end - node = node:get_parent_of_group() + node = utils.get_parent_of_group(node) local line = vim.api.nvim_get_current_line() local cursor = vim.api.nvim_win_get_cursor(0) @@ -854,7 +854,7 @@ function M.setup(conf) require("nvim-tree.keymap").setup(opts) require("nvim-tree.appearance").setup() require("nvim-tree.diagnostics").setup(opts) - require("nvim-tree.explorer"):setup(opts) + require("nvim-tree.explorer").setup(opts) require("nvim-tree.git").setup(opts) require("nvim-tree.git.utils").setup(opts) require("nvim-tree.view").setup(opts) diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua index cc75be4d..c6808eb5 100644 --- a/lua/nvim-tree/actions/fs/clipboard.lua +++ b/lua/nvim-tree/actions/fs/clipboard.lua @@ -217,10 +217,10 @@ end ---@param action ACTION ---@param action_fn fun(source: string, dest: string) function Clipboard:do_paste(node, action, action_fn) - if node.name == ".." then - node = self.explorer - else - node = node:last_group_node() + node = lib.get_last_group_node(node) + local explorer = core.get_explorer() + if node.name == ".." and explorer then + node = explorer end local clip = self.data[action] if #clip == 0 then diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua index 588c978f..dbd1bc7e 100644 --- a/lua/nvim-tree/actions/fs/create-file.lua +++ b/lua/nvim-tree/actions/fs/create-file.lua @@ -1,5 +1,6 @@ local utils = require("nvim-tree.utils") local events = require("nvim-tree.events") +local lib = require("nvim-tree.lib") local core = require("nvim-tree.core") local notify = require("nvim-tree.notify") @@ -39,13 +40,14 @@ local function get_containing_folder(node) return node.absolute_path:sub(0, -node_name_size - 1) end ----@param node Node? +---@param node Node|nil function M.fn(node) local cwd = core.get_cwd() if cwd == nil then return end + node = node and lib.get_last_group_node(node) if not node or node.name == ".." then node = { absolute_path = cwd, @@ -53,8 +55,6 @@ function M.fn(node) nodes = core.get_explorer().nodes, open = true, } - else - node = node:last_group_node() end local containing_folder = get_containing_folder(node) diff --git a/lua/nvim-tree/actions/fs/rename-file.lua b/lua/nvim-tree/actions/fs/rename-file.lua index a06fb201..9539cd7e 100644 --- a/lua/nvim-tree/actions/fs/rename-file.lua +++ b/lua/nvim-tree/actions/fs/rename-file.lua @@ -120,7 +120,7 @@ function M.fn(default_modifier) return end - node = node:last_group_node() + node = lib.get_last_group_node(node) if node.name == ".." then return end diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua index ec73d010..c38618e9 100644 --- a/lua/nvim-tree/actions/moves/item.lua +++ b/lua/nvim-tree/actions/moves/item.lua @@ -2,6 +2,7 @@ local utils = require("nvim-tree.utils") local view = require("nvim-tree.view") local core = require("nvim-tree.core") local lib = require("nvim-tree.lib") +local explorer_node = require("nvim-tree.explorer.node") local diagnostics = require("nvim-tree.diagnostics") local M = {} @@ -15,7 +16,7 @@ local MAX_DEPTH = 100 ---@return boolean local function status_is_valid(node, what, skip_gitignored) if what == "git" then - local git_status = node:get_git_status() + local git_status = explorer_node.get_git_status(node) return git_status ~= nil and (not skip_gitignored or git_status[1] ~= "!!") elseif what == "diag" then local diag_status = diagnostics.get_diag_status(node) @@ -74,7 +75,7 @@ local function expand_node(node) if not node.open then -- Expand the node. -- Should never collapse since we checked open. - node:expand_or_collapse() + lib.expand_or_collapse(node) end end @@ -97,7 +98,7 @@ local function move_next_recursive(what, skip_gitignored) valid = status_is_valid(node_init, what, skip_gitignored) end if node_init.nodes ~= nil and valid and not node_init.open then - node_init:expand_or_collapse() + lib.expand_or_collapse(node_init) end move("next", what, skip_gitignored) diff --git a/lua/nvim-tree/actions/moves/parent.lua b/lua/nvim-tree/actions/moves/parent.lua index 88eca475..e00bc49e 100644 --- a/lua/nvim-tree/actions/moves/parent.lua +++ b/lua/nvim-tree/actions/moves/parent.lua @@ -1,6 +1,7 @@ local view = require("nvim-tree.view") local utils = require("nvim-tree.utils") local core = require("nvim-tree.core") +local lib = require("nvim-tree.lib") local M = {} @@ -11,7 +12,7 @@ function M.fn(should_close) return function(node) local explorer = core.get_explorer() - node = node:last_group_node() + node = lib.get_last_group_node(node) if should_close and node.open then node.open = false if explorer then @@ -20,7 +21,7 @@ function M.fn(should_close) return end - local parent = node:get_parent_of_group().parent + local parent = utils.get_parent_of_group(node).parent if not parent or not parent.parent then return view.set_cursor({ 1, 0 }) diff --git a/lua/nvim-tree/actions/moves/sibling.lua b/lua/nvim-tree/actions/moves/sibling.lua index cf5b492d..afab9ef3 100644 --- a/lua/nvim-tree/actions/moves/sibling.lua +++ b/lua/nvim-tree/actions/moves/sibling.lua @@ -15,7 +15,7 @@ function M.fn(direction) local first, last, next, prev = nil, nil, nil, nil local found = false local parent = node.parent or core.get_explorer() - Iterator.builder(parent and parent.nodes or {}) + Iterator.builder(parent.nodes) :recursor(function() return nil end) diff --git a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua index ed898de2..25be0b90 100644 --- a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua +++ b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua @@ -1,6 +1,7 @@ local core = require("nvim-tree.core") local Iterator = require("nvim-tree.iterators.node-iterator") local notify = require("nvim-tree.notify") +local lib = require("nvim-tree.lib") local M = {} @@ -17,7 +18,7 @@ end ---@param node Node local function expand(node) - node = node:last_group_node() + node = lib.get_last_group_node(node) node.open = true if #node.nodes == 0 then core.get_explorer():expand(node) @@ -61,10 +62,10 @@ local function gen_iterator() end end ----@param node Node -function M.fn(node) +---@param base_node table +function M.fn(base_node) local explorer = core.get_explorer() - node = node.nodes and node or explorer + local node = base_node.nodes and base_node or explorer if gen_iterator()(node) then notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders") end diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua index c153c07a..27da1d20 100644 --- a/lua/nvim-tree/api.lua +++ b/lua/nvim-tree/api.lua @@ -138,7 +138,7 @@ Api.tree.change_root_to_node = wrap_node(function(node) if node.name == ".." then actions.root.change_dir.fn("..") elseif node.nodes ~= nil then - actions.root.change_dir.fn(node:last_group_node().absolute_path) + actions.root.change_dir.fn(lib.get_last_group_node(node).absolute_path) end end) @@ -198,7 +198,7 @@ Api.fs.copy.basename = wrap_node(wrap_explorer_member("clipboard", "copy_basenam Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path")) ---@param mode string ----@param node Node +---@param node table local function edit(mode, node) local path = node.absolute_path if node.link_to and not node.nodes then @@ -214,7 +214,7 @@ local function open_or_expand_or_dir_up(mode, toggle_group) if node.name == ".." then actions.root.change_dir.fn("..") elseif node.nodes then - node:expand_or_collapse(toggle_group) + lib.expand_or_collapse(node, toggle_group) elseif not toggle_group then edit(mode, node) end diff --git a/lua/nvim-tree/buffers.lua b/lua/nvim-tree/buffers.lua index 954c7e40..51ebe141 100644 --- a/lua/nvim-tree/buffers.lua +++ b/lua/nvim-tree/buffers.lua @@ -21,7 +21,7 @@ function M.reload_modified() end end ----@param node Node +---@param node table ---@return boolean function M.is_modified(node) return node @@ -32,7 +32,7 @@ function M.is_modified(node) end ---A buffer exists for the node's absolute path ----@param node Node +---@param node table ---@return boolean function M.is_opened(node) return node and vim.fn.bufloaded(node.absolute_path) > 0 diff --git a/lua/nvim-tree/core.lua b/lua/nvim-tree/core.lua index 186b1ed7..d3e6d20f 100644 --- a/lua/nvim-tree/core.lua +++ b/lua/nvim-tree/core.lua @@ -15,7 +15,7 @@ function M.init(foldername) if TreeExplorer then TreeExplorer:destroy() end - TreeExplorer = require("nvim-tree.explorer"):create(foldername) + TreeExplorer = require("nvim-tree.explorer"):new(foldername) if not first_init_done then events._dispatch_ready() first_init_done = true diff --git a/lua/nvim-tree/enum.lua b/lua/nvim-tree/enum.lua index a680c2b3..9c50bc27 100644 --- a/lua/nvim-tree/enum.lua +++ b/lua/nvim-tree/enum.lua @@ -1,13 +1,5 @@ local M = {} ----Must be synced with uv.fs_stat.result as it is compared with it ----@enum (key) NODE_TYPE -M.NODE_TYPE = { - directory = 1, - file = 2, - link = 4, -} - ---Setup options for "highlight_*" ---@enum HL_POSITION M.HL_POSITION = { diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua index 0045ba9b..6ae149b6 100644 --- a/lua/nvim-tree/explorer/init.lua +++ b/lua/nvim-tree/explorer/init.lua @@ -1,15 +1,15 @@ +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 view = require("nvim-tree.view") -local node_factory = require("nvim-tree.node.factory") - -local RootNode = require("nvim-tree.node.root") -local Watcher = require("nvim-tree.watcher") +local watch = require("nvim-tree.explorer.watch") +local explorer_node = require("nvim-tree.explorer.node") local Iterator = require("nvim-tree.iterators.node-iterator") local NodeIterator = require("nvim-tree.iterators.node-iterator") +local Watcher = require("nvim-tree.watcher") local Filters = require("nvim-tree.explorer.filters") local Marks = require("nvim-tree.marks") @@ -22,20 +22,23 @@ local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON local config ----@class (exact) Explorer: RootNode +---@class Explorer ---@field opts table user options +---@field absolute_path string +---@field nodes Node[] +---@field open boolean +---@field watcher Watcher|nil ---@field renderer Renderer ---@field filters Filters ---@field live_filter LiveFilter ---@field sorters Sorter ---@field marks Marks ---@field clipboard Clipboard -local Explorer = RootNode:new() +local Explorer = {} ----Static factory method ----@param path string? ----@return Explorer? -function Explorer:create(path) +---@param path string|nil +---@return Explorer|nil +function Explorer:new(path) local err if path then @@ -45,22 +48,21 @@ function Explorer:create(path) end if not path then notify.error(err) - return nil + return end - ---@type Explorer - local explorer_placeholder = nil + local o = { + opts = config, + absolute_path = path, + nodes = {}, + open = true, + sorters = Sorters:new(config), + } - local o = RootNode:create(explorer_placeholder, path, "..", nil) + setmetatable(o, self) + self.__index = self - o = self:new(o) --[[@as Explorer]] - - o.explorer = o - - o.open = true - o.opts = config - - o.sorters = Sorters:new(config) + o.watcher = watch.create_watcher(o) o.renderer = Renderer:new(config, o) o.filters = Filters:new(config, o) o.live_filter = LiveFilter:new(config, o) @@ -77,6 +79,18 @@ function Explorer:expand(node) self:_load(node) end +function Explorer:destroy() + local function iterate(node) + explorer_node.node_destroy(node) + if node.nodes then + for _, child in pairs(node.nodes) do + iterate(child) + end + end + end + iterate(self) +end + ---@param node Node ---@param git_status table|nil function Explorer:reload(node, git_status) @@ -97,7 +111,7 @@ function Explorer:reload(node, git_status) local remain_childs = {} - local node_ignored = node:is_git_ignored() + local node_ignored = explorer_node.is_git_ignored(node) ---@type table local nodes_by_path = utils.key_by(node.nodes, "absolute_path") @@ -124,19 +138,32 @@ function Explorer:reload(node, git_status) if filter_reason == FILTER_REASON.none then remain_childs[abs] = true + -- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility + local t = stat and stat.type or nil + -- Recreate node if type changes. if nodes_by_path[abs] then local n = nodes_by_path[abs] - if not stat or n.type ~= stat.type then + if n.type ~= t then utils.array_remove(node.nodes, n) - n:destroy() + explorer_node.node_destroy(n) nodes_by_path[abs] = nil end end if not nodes_by_path[abs] then - local new_child = node_factory.create_node(self, node, abs, stat, name) + 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 @@ -144,7 +171,7 @@ function Explorer:reload(node, git_status) else local n = nodes_by_path[abs] if n then - n.executable = utils.is_executable(abs) or false + n.executable = builders.is_executable(abs) or false n.fs_stat = stat end end @@ -163,14 +190,14 @@ function Explorer:reload(node, git_status) if remain_childs[n.absolute_path] then return remain_childs[n.absolute_path] else - n:destroy() + explorer_node.node_destroy(n) return false end end, node.nodes) ) local is_root = not node.parent - local child_folder_only = node:has_one_child_folder() and node.nodes[1] + local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] if config.renderer.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) @@ -185,6 +212,26 @@ function Explorer:reload(node, git_status) 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 @@ -212,7 +259,7 @@ function Explorer:refresh_parent_nodes_for_path(path) local project = git.get_project(toplevel) or {} self:reload(node, project) - node:update_parent_statuses(project, toplevel) + self:update_parent_statuses(node, project, toplevel) end log.profile_end(profile) @@ -227,19 +274,65 @@ function Explorer:_load(node) end ---@private ----@param nodes_by_path Node[] +---@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 - node:update_git_status(node_ignored, status) + 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 + ---@private ---@param handle uv.uv_fs_t ---@param cwd string @@ -247,7 +340,7 @@ end ---@param git_status table ---@param parent Explorer function Explorer:populate_children(handle, cwd, node, git_status, parent) - local node_ignored = node:is_git_ignored() + local node_ignored = explorer_node.is_git_ignored(node) local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") local filter_status = parent.filters:prepare(git_status) @@ -275,11 +368,23 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent) local stat = vim.loop.fs_lstat(abs) local filter_reason = parent.filters:should_filter_as_reason(abs, stat, filter_status) if filter_reason == FILTER_REASON.none and not nodes_by_path[abs] then - local child = node_factory.create_node(self, node, abs, stat, name) + -- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility + local t = stat and stat.type or nil + local child = nil + if t == "directory" and vim.loop.fs_access(abs, "R") then + child = builders.folder(node, abs, name, stat) + elseif t == "file" then + 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 + child = link + end + end if child then table.insert(node.nodes, child) nodes_by_path[child.absolute_path] = true - child:update_git_status(node_ignored, git_status) + explorer_node.update_git_status(child, node_ignored, git_status) end else for reason, value in pairs(FILTER_REASON) do @@ -311,7 +416,7 @@ function Explorer:explore(node, status, parent) self:populate_children(handle, cwd, node, status, parent) local is_root = not node.parent - local child_folder_only = node:has_one_child_folder() and node.nodes[1] + local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] if config.renderer.group_empty and not is_root and child_folder_only then local child_cwd = child_folder_only.link_to or child_folder_only.absolute_path local child_status = git.load_project_status(child_cwd) @@ -368,13 +473,14 @@ function Explorer:reload_git() event_running = true local projects = git.reload() - self:reload_node_status(projects) + explorer_node.reload_node_status(self, projects) self.renderer:draw() event_running = false end -function Explorer:setup(opts) +function Explorer.setup(opts) config = opts + require("nvim-tree.explorer.node").setup(opts) require("nvim-tree.explorer.watch").setup(opts) end diff --git a/lua/nvim-tree/explorer/live-filter.lua b/lua/nvim-tree/explorer/live-filter.lua index ca772b34..7cdfc35d 100644 --- a/lua/nvim-tree/explorer/live-filter.lua +++ b/lua/nvim-tree/explorer/live-filter.lua @@ -23,7 +23,7 @@ function LiveFilter:new(opts, explorer) return o end ----@param node_ Node? +---@param node_ Node|nil local function reset_filter(self, node_) node_ = node_ or self.explorer @@ -85,7 +85,7 @@ local function matches(self, node) return vim.regex(self.filter):match_str(name) ~= nil end ----@param node_ Node? +---@param node_ Node|nil function LiveFilter:apply_filter(node_) if not self.filter or self.filter == "" then reset_filter(self, node_) diff --git a/lua/nvim-tree/explorer/node-builders.lua b/lua/nvim-tree/explorer/node-builders.lua new file mode 100644 index 00000000..10e95816 --- /dev/null +++ b/lua/nvim-tree/explorer/node-builders.lua @@ -0,0 +1,107 @@ +local utils = require("nvim-tree.utils") +local watch = require("nvim-tree.explorer.watch") + +local M = {} + +---@param parent Node +---@param absolute_path string +---@param name string +---@param fs_stat uv.fs_stat.result|nil +---@return Node +function M.folder(parent, absolute_path, name, fs_stat) + local handle = vim.loop.fs_scandir(absolute_path) + local has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil + + local node = { + type = "directory", + absolute_path = absolute_path, + fs_stat = fs_stat, + group_next = nil, -- If node is grouped, this points to the next child dir/link node + has_children = has_children, + name = name, + nodes = {}, + open = false, + parent = parent, + } + + node.watcher = watch.create_watcher(node) + + return node +end + +--- path is an executable file or directory +---@param absolute_path string +---@return boolean|nil +function M.is_executable(absolute_path) + if utils.is_windows or utils.is_wsl then + --- executable detection on windows is buggy and not performant hence it is disabled + return false + else + return vim.loop.fs_access(absolute_path, "X") + end +end + +---@param parent Node +---@param absolute_path string +---@param name string +---@param fs_stat uv.fs_stat.result|nil +---@return Node +function M.file(parent, absolute_path, name, fs_stat) + local ext = string.match(name, ".?[^.]+%.(.*)") or "" + + return { + type = "file", + absolute_path = absolute_path, + executable = M.is_executable(absolute_path), + extension = ext, + fs_stat = fs_stat, + name = name, + parent = parent, + } +end + +-- TODO-INFO: sometimes fs_realpath returns nil +-- I expect this be a bug in glibc, because it fails to retrieve the path for some +-- links (for instance libr2.so in /usr/lib) and thus even with a C program realpath fails +-- when it has no real reason to. Maybe there is a reason, but errno is definitely wrong. +-- So we need to check for link_to ~= nil when adding new links to the main tree +---@param parent Node +---@param absolute_path string +---@param name string +---@param fs_stat uv.fs_stat.result|nil +---@return Node +function M.link(parent, absolute_path, name, fs_stat) + --- I dont know if this is needed, because in my understanding, there isn't hard links in windows, but just to be sure i changed it. + local link_to = vim.loop.fs_realpath(absolute_path) + local open, nodes, has_children + + local is_dir_link = (link_to ~= nil) and vim.loop.fs_stat(link_to).type == "directory" + + if is_dir_link and link_to then + local handle = vim.loop.fs_scandir(link_to) + has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil + open = false + nodes = {} + end + + local node = { + type = "link", + absolute_path = absolute_path, + fs_stat = fs_stat, + group_next = nil, -- If node is grouped, this points to the next child dir/link node + has_children = has_children, + link_to = link_to, + name = name, + nodes = nodes, + open = open, + parent = parent, + } + + if is_dir_link then + node.watcher = watch.create_watcher(node) + end + + return node +end + +return M diff --git a/lua/nvim-tree/explorer/node.lua b/lua/nvim-tree/explorer/node.lua new file mode 100644 index 00000000..27e31b13 --- /dev/null +++ b/lua/nvim-tree/explorer/node.lua @@ -0,0 +1,183 @@ +local git = {} -- circular dependencies + +local M = {} + +---@class GitStatus +---@field file string|nil +---@field dir table|nil + +---@param parent_ignored boolean +---@param status table|nil +---@param absolute_path string +---@return GitStatus|nil +local function get_dir_git_status(parent_ignored, status, absolute_path) + if parent_ignored then + return { file = "!!" } + end + + if status then + return { + file = status.files and status.files[absolute_path], + dir = status.dirs and { + direct = status.dirs.direct[absolute_path], + indirect = status.dirs.indirect[absolute_path], + }, + } + end +end + +---@param parent_ignored boolean +---@param status table +---@param absolute_path string +---@return GitStatus +local function get_git_status(parent_ignored, status, absolute_path) + local file_status = parent_ignored and "!!" or (status and status.files and status.files[absolute_path]) + return { file = file_status } +end + +---@param node Node +---@return boolean +function M.has_one_child_folder(node) + return #node.nodes == 1 and node.nodes[1].nodes and vim.loop.fs_access(node.nodes[1].absolute_path, "R") or false +end + +---@param node Node +---@param parent_ignored boolean +---@param status table|nil +function M.update_git_status(node, parent_ignored, status) + local get_status + if node.nodes then + get_status = get_dir_git_status + else + get_status = get_git_status + end + + -- status of the node's absolute path + node.git_status = get_status(parent_ignored, status, node.absolute_path) + + -- status of the link target, if the link itself is not dirty + if node.link_to and not node.git_status then + node.git_status = get_status(parent_ignored, status, node.link_to) + end +end + +---@param node Node +---@return GitStatus|nil +function M.get_git_status(node) + local git_status = node and node.git_status + if not git_status then + -- status doesn't exist + return nil + end + + if not node.nodes then + -- file + return git_status.file and { git_status.file } + end + + -- dir + if not M.config.git.show_on_dirs then + return nil + end + + local status = {} + if not require("nvim-tree.lib").get_last_group_node(node).open or M.config.git.show_on_open_dirs then + -- dir is closed or we should show on open_dirs + if git_status.file ~= nil then + table.insert(status, git_status.file) + end + if git_status.dir ~= nil then + if git_status.dir.direct ~= nil then + for _, s in pairs(node.git_status.dir.direct) do + table.insert(status, s) + end + end + if git_status.dir.indirect ~= nil then + for _, s in pairs(node.git_status.dir.indirect) do + table.insert(status, s) + end + end + end + else + -- dir is open and we shouldn't show on open_dirs + if git_status.file ~= nil then + table.insert(status, git_status.file) + end + if git_status.dir ~= nil and git_status.dir.direct ~= nil then + local deleted = { + [" D"] = true, + ["D "] = true, + ["RD"] = true, + ["DD"] = true, + } + for _, s in pairs(node.git_status.dir.direct) do + if deleted[s] then + table.insert(status, s) + end + end + end + end + if #status == 0 then + return nil + else + return status + end +end + +---@param parent_node Node|nil +---@param projects table +function M.reload_node_status(parent_node, projects) + if parent_node == nil then + return + end + + local toplevel = git.get_toplevel(parent_node.absolute_path) + local status = projects[toplevel] or {} + for _, node in ipairs(parent_node.nodes) do + M.update_git_status(node, M.is_git_ignored(parent_node), status) + if node.nodes and #node.nodes > 0 then + M.reload_node_status(node, projects) + end + end +end + +---@param node Node +---@return boolean +function M.is_git_ignored(node) + return node and node.git_status ~= nil and node.git_status.file == "!!" +end + +---@param node Node +---@return boolean +function M.is_dotfile(node) + if node == nil then + return false + end + if node.is_dot or (node.name and (node.name:sub(1, 1) == ".")) or M.is_dotfile(node.parent) then + node.is_dot = true + return true + end + return false +end + +---@param node Node +function M.node_destroy(node) + if not node then + return + end + + if node.watcher then + node.watcher:destroy() + node.watcher = nil + end +end + +function M.setup(opts) + M.config = { + git = opts.git, + } + + git = require("nvim-tree.git") +end + +return M diff --git a/lua/nvim-tree/explorer/sorters.lua b/lua/nvim-tree/explorer/sorters.lua index bcf55900..4ec4c3d5 100644 --- a/lua/nvim-tree/explorer/sorters.lua +++ b/lua/nvim-tree/explorer/sorters.lua @@ -111,7 +111,7 @@ local function split_merge(t, first, last, comparator) end ---Perform a merge sort using sorter option. ----@param t Node[] +---@param t table nodes function Sorter:sort(t) if self.user then local t_user = {} diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua index 7fd13f4c..317f3e54 100644 --- a/lua/nvim-tree/explorer/watch.lua +++ b/lua/nvim-tree/explorer/watch.lua @@ -76,7 +76,12 @@ function M.create_watcher(node) else log.line("watcher", "node event executing refresh '%s'", node.absolute_path) end - node:refresh() + local explorer = require("nvim-tree.core").get_explorer() + if explorer then + explorer:refresh_node(node, function() + explorer.renderer:draw() + end) + end end) end diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index b963cfd7..caea8517 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -4,10 +4,7 @@ local git_utils = require("nvim-tree.git.utils") local Runner = require("nvim-tree.git.runner") local Watcher = require("nvim-tree.watcher").Watcher local Iterator = require("nvim-tree.iterators.node-iterator") - ----@class GitStatus ----@field file string|nil ----@field dir table|nil +local explorer_node = require("nvim-tree.explorer.node") local M = { config = {}, @@ -211,15 +208,18 @@ local function reload_tree_at(toplevel) Iterator.builder(root_node.nodes) :hidden() :applier(function(node) - local parent_ignored = node.parent and node.parent:is_git_ignored() or false - node:update_git_status(parent_ignored, git_status) + local parent_ignored = explorer_node.is_git_ignored(node.parent) + explorer_node.update_git_status(node, parent_ignored, git_status) end) :recursor(function(node) return node.nodes and #node.nodes > 0 and node.nodes end) :iterate() - root_node.explorer.renderer:draw() + local explorer = require("nvim-tree.core").get_explorer() + if explorer then + explorer.renderer:draw() + end end) end @@ -283,35 +283,6 @@ function M.load_project_status(path) end end ----@param parent_ignored boolean ----@param status table|nil ----@param absolute_path string ----@return GitStatus|nil -function M.git_status_dir(parent_ignored, status, absolute_path) - if parent_ignored then - return { file = "!!" } - end - - if status then - return { - file = status.files and status.files[absolute_path], - dir = status.dirs and { - direct = status.dirs.direct[absolute_path], - indirect = status.dirs.indirect[absolute_path], - }, - } - end -end - ----@param parent_ignored boolean ----@param status table|nil ----@param absolute_path string ----@return GitStatus -function M.git_status_file(parent_ignored, status, absolute_path) - local file_status = parent_ignored and "!!" or (status and status.files and status.files[absolute_path]) - return { file = file_status } -end - function M.purge_state() log.line("git", "purge_state") diff --git a/lua/nvim-tree/lib.lua b/lua/nvim-tree/lib.lua index 15ccb337..0ad23687 100644 --- a/lua/nvim-tree/lib.lua +++ b/lua/nvim-tree/lib.lua @@ -3,6 +3,7 @@ local core = require("nvim-tree.core") local utils = require("nvim-tree.utils") local events = require("nvim-tree.events") local notify = require("nvim-tree.notify") +local explorer_node = require("nvim-tree.explorer.node") ---@class LibOpenOpts ---@field path string|nil path @@ -14,7 +15,6 @@ local M = { } ---Cursor position as per vim.api.nvim_win_get_cursor ----nil on no explorer or invalid view win ---@return integer[]|nil function M.get_cursor_position() if not core.get_explorer() then @@ -31,28 +31,154 @@ end ---@return Node|nil function M.get_node_at_cursor() - local explorer = core.get_explorer() - if not explorer then - return - end - local cursor = M.get_cursor_position() if not cursor then return end if cursor[1] == 1 and view.is_root_folder_visible(core.get_cwd()) then - return explorer + return { name = ".." } end - return utils.get_nodes_by_line(explorer.nodes, core.get_nodes_starting_line())[cursor[1]] + return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[cursor[1]] +end + +---Create a sanitized partial copy of a node, populating children recursively. +---@param node Node|nil +---@return Node|nil cloned node +local function clone_node(node) + if not node then + node = core.get_explorer() + if not node then + return nil + end + end + + local n = { + absolute_path = node.absolute_path, + executable = node.executable, + extension = node.extension, + git_status = node.git_status, + has_children = node.has_children, + hidden = node.hidden, + link_to = node.link_to, + name = node.name, + open = node.open, + type = node.type, + fs_stat = node.fs_stat, + } + + if type(node.nodes) == "table" then + n.nodes = {} + for _, child in ipairs(node.nodes) do + table.insert(n.nodes, clone_node(child)) + end + end + + return n end ---Api.tree.get_nodes ----@return Node[]? +---@return Node[]|nil function M.get_nodes() + return clone_node(core.get_explorer()) +end + +-- If node is grouped, return the last node in the group. Otherwise, return the given node. +---@param node Node +---@return Node +function M.get_last_group_node(node) + while node and node.group_next do + node = node.group_next + end + + return node ---@diagnostic disable-line: return-type-mismatch -- it can't be nil +end + +---Group empty folders +-- Recursively group nodes +---@param node Node +---@return Node[] +function M.group_empty_folders(node) + local is_root = not node.parent + local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] + if M.group_empty and not is_root and child_folder_only then + node.group_next = child_folder_only + local ns = M.group_empty_folders(child_folder_only) + node.nodes = ns or {} + return ns + end + return node.nodes +end + +---Ungroup empty folders +-- If a node is grouped, ungroup it: put node.group_next to the node.nodes and set node.group_next to nil +---@param node Node +function M.ungroup_empty_folders(node) + local cur = node + while cur and cur.group_next do + cur.nodes = { cur.group_next } + cur.group_next = nil + cur = cur.nodes[1] + end +end + +---@param node Node +---@return Node[] +function M.get_all_nodes_in_group(node) + local next_node = utils.get_parent_of_group(node) + local nodes = {} + while next_node do + table.insert(nodes, next_node) + next_node = next_node.group_next + end + return nodes +end + +-- Toggle group empty folders +---@param head_node Node +local function toggle_group_folders(head_node) + local is_grouped = head_node.group_next ~= nil + + if is_grouped then + M.ungroup_empty_folders(head_node) + else + M.group_empty_folders(head_node) + end +end + +---@param node Node +function M.expand_or_collapse(node, toggle_group) local explorer = core.get_explorer() - return explorer and explorer:clone() + + toggle_group = toggle_group or false + if node.has_children then + node.has_children = false + end + + if #node.nodes == 0 and explorer then + explorer:expand(node) + end + + local head_node = utils.get_parent_of_group(node) + if toggle_group then + toggle_group_folders(head_node) + end + + local open = M.get_last_group_node(node).open + local next_open + if toggle_group then + next_open = open + else + next_open = not open + end + for _, n in ipairs(M.get_all_nodes_in_group(head_node)) do + n.open = next_open + end + + if explorer then + explorer.renderer:draw() + end end function M.set_target_win() diff --git a/lua/nvim-tree/log.lua b/lua/nvim-tree/log.lua index 8e796b9e..ad07a856 100644 --- a/lua/nvim-tree/log.lua +++ b/lua/nvim-tree/log.lua @@ -71,7 +71,7 @@ end --- Write to log file the inspection of a node --- defaults to the node under cursor if none is provided ---@param typ string as per log.types config ----@param node Node? node to be inspected +---@param node table|nil node to be inspected ---@param fmt string for string.format ---@vararg any arguments for string.format function M.node(typ, node, fmt, ...) diff --git a/lua/nvim-tree/marks/init.lua b/lua/nvim-tree/marks/init.lua index c2da9009..8fec6c66 100644 --- a/lua/nvim-tree/marks/init.lua +++ b/lua/nvim-tree/marks/init.lua @@ -8,8 +8,6 @@ local rename_file = require("nvim-tree.actions.fs.rename-file") local trash = require("nvim-tree.actions.fs.trash") local utils = require("nvim-tree.utils") -local DirectoryNode = require("nvim-tree.node.directory") - ---@class Marks ---@field config table hydrated user opts.filters ---@field private explorer Explorer @@ -154,7 +152,7 @@ function Marks:bulk_move() local node_at_cursor = lib.get_node_at_cursor() local default_path = core.get_cwd() - if node_at_cursor and node_at_cursor:is(DirectoryNode) then + if node_at_cursor and node_at_cursor.type == "directory" then default_path = node_at_cursor.absolute_path elseif node_at_cursor and node_at_cursor.parent then default_path = node_at_cursor.parent.absolute_path diff --git a/lua/nvim-tree/node.lua b/lua/nvim-tree/node.lua new file mode 100644 index 00000000..cf8aa671 --- /dev/null +++ b/lua/nvim-tree/node.lua @@ -0,0 +1,36 @@ +---@meta + +---@class ParentNode +---@field name string + +---@class BaseNode +---@field absolute_path string +---@field executable boolean +---@field fs_stat uv.fs_stat.result|nil +---@field git_status GitStatus|nil +---@field hidden boolean +---@field is_dot boolean +---@field name string +---@field parent DirNode +---@field type string +---@field watcher function|nil +---@field diag_status DiagStatus|nil + +---@class DirNode: BaseNode +---@field has_children boolean +---@field group_next Node|nil +---@field nodes Node[] +---@field open boolean +---@field hidden_stats table -- Each field of this table is a key for source and value for count + +---@class FileNode: BaseNode +---@field extension string + +---@class SymlinkDirNode: DirNode +---@field link_to string + +---@class SymlinkFileNode: FileNode +---@field link_to string + +---@alias SymlinkNode SymlinkDirNode|SymlinkFileNode +---@alias Node ParentNode|DirNode|FileNode|SymlinkNode|Explorer diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua deleted file mode 100644 index 050d3e35..00000000 --- a/lua/nvim-tree/node/directory.lua +++ /dev/null @@ -1,79 +0,0 @@ -local watch = require("nvim-tree.explorer.watch") - -local BaseNode = require("nvim-tree.node") - ----@class (exact) DirectoryNode: BaseNode ----@field has_children boolean ----@field group_next Node? -- If node is grouped, this points to the next child dir/link node ----@field nodes Node[] ----@field open boolean ----@field hidden_stats table? -- Each field of this table is a key for source and value for count -local DirectoryNode = BaseNode:new() - ----Static factory method ----@param explorer Explorer ----@param parent Node? ----@param absolute_path string ----@param name string ----@param fs_stat uv.fs_stat.result|nil ----@return DirectoryNode -function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat) - local handle = vim.loop.fs_scandir(absolute_path) - local has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil or false - - ---@type DirectoryNode - local o = { - type = "directory", - explorer = explorer, - absolute_path = absolute_path, - executable = false, - fs_stat = fs_stat, - git_status = nil, - hidden = false, - is_dot = false, - name = name, - parent = parent, - watcher = nil, - diag_status = nil, - - has_children = has_children, - group_next = nil, - nodes = {}, - open = false, - hidden_stats = nil, - } - o = self:new(o) --[[@as DirectoryNode]] - - o.watcher = watch.create_watcher(o) - - return o -end - -function DirectoryNode:destroy() - BaseNode.destroy(self) - if self.nodes then - for _, node in pairs(self.nodes) do - node:destroy() - end - end -end - ----Create a sanitized partial copy of a node, populating children recursively. ----@return DirectoryNode cloned -function DirectoryNode:clone() - local clone = BaseNode.clone(self) --[[@as DirectoryNode]] - - clone.has_children = self.has_children - clone.group_next = nil - clone.nodes = {} - clone.open = self.open - clone.hidden_stats = nil - - for _, child in ipairs(self.nodes) do - table.insert(clone.nodes, child:clone()) - end - - return clone -end - -return DirectoryNode diff --git a/lua/nvim-tree/node/factory.lua b/lua/nvim-tree/node/factory.lua deleted file mode 100644 index a46057da..00000000 --- a/lua/nvim-tree/node/factory.lua +++ /dev/null @@ -1,31 +0,0 @@ -local DirectoryNode = require("nvim-tree.node.directory") -local LinkNode = require("nvim-tree.node.link") -local FileNode = require("nvim-tree.node.file") -local Watcher = require("nvim-tree.watcher") - -local M = {} - ----Factory function to create the appropriate Node ----@param explorer Explorer ----@param parent Node ----@param abs string ----@param stat uv.fs_stat.result? -- on nil stat return nil Node ----@param name string ----@return Node? -function M.create_node(explorer, parent, abs, stat, name) - if not stat then - return nil - end - - if stat.type == "directory" and vim.loop.fs_access(abs, "R") and Watcher.is_fs_event_capable(abs) then - return DirectoryNode:create(explorer, parent, abs, name, stat) - elseif stat.type == "file" then - return FileNode:create(explorer, parent, abs, name, stat) - elseif stat.type == "link" then - return LinkNode:create(explorer, parent, abs, name, stat) - end - - return nil -end - -return M diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua deleted file mode 100644 index f504631b..00000000 --- a/lua/nvim-tree/node/file.lua +++ /dev/null @@ -1,49 +0,0 @@ -local utils = require("nvim-tree.utils") - -local BaseNode = require("nvim-tree.node") - ----@class (exact) FileNode: BaseNode ----@field extension string -local FileNode = BaseNode:new() - ----Static factory method ----@param explorer Explorer ----@param parent Node ----@param absolute_path string ----@param name string ----@param fs_stat uv.fs_stat.result? ----@return FileNode -function FileNode:create(explorer, parent, absolute_path, name, fs_stat) - ---@type FileNode - local o = { - type = "file", - explorer = explorer, - absolute_path = absolute_path, - executable = utils.is_executable(absolute_path), - fs_stat = fs_stat, - git_status = nil, - hidden = false, - is_dot = false, - name = name, - parent = parent, - watcher = nil, - diag_status = nil, - - extension = string.match(name, ".?[^.]+%.(.*)") or "", - } - o = self:new(o) --[[@as FileNode]] - - return o -end - ----Create a sanitized partial copy of a node, populating children recursively. ----@return FileNode cloned -function FileNode:clone() - local clone = BaseNode.clone(self) --[[@as FileNode]] - - clone.extension = self.extension - - return clone -end - -return FileNode diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua deleted file mode 100644 index b915df7d..00000000 --- a/lua/nvim-tree/node/init.lua +++ /dev/null @@ -1,347 +0,0 @@ -local git = require("nvim-tree.git") - ----Abstract Node class. ----Uses the abstract factory pattern to instantiate child instances. ----@class (exact) BaseNode ----@field private __index? table ----@field type NODE_TYPE ----@field explorer Explorer ----@field absolute_path string ----@field executable boolean ----@field fs_stat uv.fs_stat.result? ----@field git_status GitStatus? ----@field hidden boolean ----@field is_dot boolean ----@field name string ----@field parent Node? ----@field watcher Watcher? ----@field diag_status DiagStatus? -local BaseNode = {} - ----@alias Node RootNode|BaseNode|DirectoryNode|FileNode|LinkNode - ----@param o BaseNode? ----@return BaseNode -function BaseNode:new(o) - o = o or {} - - setmetatable(o, self) - self.__index = self - - return o -end - -function BaseNode:destroy() - if self.watcher then - self.watcher:destroy() - self.watcher = nil - end -end - ----From plenary ----Checks if the object is an instance ----This will start with the lowest class and loop over all the superclasses. ----@param self BaseNode ----@param T BaseNode ----@return boolean -function BaseNode:is(T) - local mt = getmetatable(self) - while mt do - if mt == T then - return true - end - mt = getmetatable(mt) - end - return false -end - ----@return boolean -function BaseNode:has_one_child_folder() - return #self.nodes == 1 and self.nodes[1].nodes and vim.loop.fs_access(self.nodes[1].absolute_path, "R") or false -end - ----@param parent_ignored boolean ----@param status table|nil -function BaseNode:update_git_status(parent_ignored, status) - local get_status - if self.nodes then - get_status = git.git_status_dir - else - get_status = git.git_status_file - end - - -- status of the node's absolute path - self.git_status = get_status(parent_ignored, status, self.absolute_path) - - -- status of the link target, if the link itself is not dirty - if self.link_to and not self.git_status then - self.git_status = get_status(parent_ignored, status, self.link_to) - end -end - ----@return GitStatus|nil -function BaseNode:get_git_status() - if not self.git_status then - -- status doesn't exist - return nil - end - - if not self.nodes then - -- file - return self.git_status.file and { self.git_status.file } - end - - -- dir - if not self.explorer.opts.git.show_on_dirs then - return nil - end - - local status = {} - if not self:last_group_node().open or self.explorer.opts.git.show_on_open_dirs then - -- dir is closed or we should show on open_dirs - if self.git_status.file ~= nil then - table.insert(status, self.git_status.file) - end - if self.git_status.dir ~= nil then - if self.git_status.dir.direct ~= nil then - for _, s in pairs(self.git_status.dir.direct) do - table.insert(status, s) - end - end - if self.git_status.dir.indirect ~= nil then - for _, s in pairs(self.git_status.dir.indirect) do - table.insert(status, s) - end - end - end - else - -- dir is open and we shouldn't show on open_dirs - if self.git_status.file ~= nil then - table.insert(status, self.git_status.file) - end - if self.git_status.dir ~= nil and self.git_status.dir.direct ~= nil then - local deleted = { - [" D"] = true, - ["D "] = true, - ["RD"] = true, - ["DD"] = true, - } - for _, s in pairs(self.git_status.dir.direct) do - if deleted[s] then - table.insert(status, s) - end - end - end - end - if #status == 0 then - return nil - else - return status - end -end - ----@param projects table -function BaseNode:reload_node_status(projects) - local toplevel = git.get_toplevel(self.absolute_path) - local status = projects[toplevel] or {} - for _, node in ipairs(self.nodes) do - node:update_git_status(self:is_git_ignored(), status) - if node.nodes and #node.nodes > 0 then - self:reload_node_status(projects) - end - end -end - ----@return boolean -function BaseNode:is_git_ignored() - return self.git_status ~= nil and self.git_status.file == "!!" -end - ----@return boolean -function BaseNode:is_dotfile() - if - self.is_dot -- - or (self.name and (self.name:sub(1, 1) == ".")) -- - or (self.parent and self.parent:is_dotfile()) - then - self.is_dot = true - return true - end - return false -end - --- If node is grouped, return the last node in the group. Otherwise, return the given node. ----@return Node -function BaseNode:last_group_node() - local node = self --[[@as BaseNode]] - - while node.group_next do - node = node.group_next - end - - return node -end - ----@param project table|nil ----@param root string|nil -function BaseNode:update_parent_statuses(project, root) - local node = self - 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 - node:update_git_status(node.parent and node.parent:is_git_ignored() or false, project) - - -- maybe parent - node = node.parent - end -end - ----Refresh contents and git status for a single node -function BaseNode:refresh() - local parent_node = self:get_parent_of_group() - local toplevel = git.get_toplevel(self.absolute_path) - - git.reload_project(toplevel, self.absolute_path, function() - local project = git.get_project(toplevel) or {} - - self.explorer:reload(parent_node, project) - - parent_node:update_parent_statuses(project, toplevel) - - self.explorer.renderer:draw() - end) -end - ----Get the highest parent of grouped nodes ----@return Node node or parent -function BaseNode:get_parent_of_group() - local node = self - while node and node.parent and node.parent.group_next do - node = node.parent or node - end - return node -end - ----@return Node[] -function BaseNode:get_all_nodes_in_group() - local next_node = self:get_parent_of_group() - local nodes = {} - while next_node do - table.insert(nodes, next_node) - next_node = next_node.group_next - end - return nodes -end - --- Toggle group empty folders -function BaseNode:toggle_group_folders() - local is_grouped = self.group_next ~= nil - - if is_grouped then - self:ungroup_empty_folders() - else - self:group_empty_folders() - end -end - ----Group empty folders --- Recursively group nodes ----@return Node[] -function BaseNode:group_empty_folders() - local is_root = not self.parent - local child_folder_only = self:has_one_child_folder() and self.nodes[1] - if self.explorer.opts.renderer.group_empty and not is_root and child_folder_only then - ---@cast self DirectoryNode -- TODO move this to the class - self.group_next = child_folder_only - local ns = child_folder_only:group_empty_folders() - self.nodes = ns or {} - return ns - end - return self.nodes -end - ----Ungroup empty folders --- If a node is grouped, ungroup it: put node.group_next to the node.nodes and set node.group_next to nil -function BaseNode:ungroup_empty_folders() - local cur = self - while cur and cur.group_next do - cur.nodes = { cur.group_next } - cur.group_next = nil - cur = cur.nodes[1] - end -end - -function BaseNode:expand_or_collapse(toggle_group) - toggle_group = toggle_group or false - if self.has_children then - ---@cast self DirectoryNode -- TODO move this to the class - self.has_children = false - end - - if #self.nodes == 0 then - self.explorer:expand(self) - end - - local head_node = self:get_parent_of_group() - if toggle_group then - head_node:toggle_group_folders() - end - - local open = self:last_group_node().open - local next_open - if toggle_group then - next_open = open - else - next_open = not open - end - for _, n in ipairs(head_node:get_all_nodes_in_group()) do - n.open = next_open - end - - self.explorer.renderer:draw() -end - ----Create a sanitized partial copy of a node, populating children recursively. ----@return BaseNode cloned -function BaseNode:clone() - ---@type Explorer - local explorer_placeholder = nil - - ---@type BaseNode - local clone = { - type = self.type, - explorer = explorer_placeholder, - absolute_path = self.absolute_path, - executable = self.executable, - fs_stat = self.fs_stat, - git_status = self.git_status, - hidden = self.hidden, - is_dot = self.is_dot, - name = self.name, - parent = nil, - watcher = nil, - diag_status = nil, - } - - return clone -end - -return BaseNode diff --git a/lua/nvim-tree/node/link.lua b/lua/nvim-tree/node/link.lua deleted file mode 100644 index 2df9d920..00000000 --- a/lua/nvim-tree/node/link.lua +++ /dev/null @@ -1,89 +0,0 @@ -local watch = require("nvim-tree.explorer.watch") - -local BaseNode = require("nvim-tree.node") - ----@class (exact) LinkNode: BaseNode ----@field has_children boolean ----@field group_next Node? -- If node is grouped, this points to the next child dir/link node ----@field link_to string absolute path ----@field nodes Node[] ----@field open boolean -local LinkNode = BaseNode:new() - ----Static factory method ----@param explorer Explorer ----@param parent Node ----@param absolute_path string ----@param name string ----@param fs_stat uv.fs_stat.result? ----@return LinkNode? nil on vim.loop.fs_realpath failure -function LinkNode:create(explorer, parent, absolute_path, name, fs_stat) - -- INFO: sometimes fs_realpath returns nil - -- I expect this be a bug in glibc, because it fails to retrieve the path for some - -- links (for instance libr2.so in /usr/lib) and thus even with a C program realpath fails - -- when it has no real reason to. Maybe there is a reason, but errno is definitely wrong. - local link_to = vim.loop.fs_realpath(absolute_path) - if not link_to then - return nil - end - - local open, nodes, has_children - local is_dir_link = (link_to ~= nil) and vim.loop.fs_stat(link_to).type == "directory" - - if is_dir_link and link_to then - local handle = vim.loop.fs_scandir(link_to) - has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil or false - open = false - nodes = {} - end - - ---@type LinkNode - local o = { - type = "link", - explorer = explorer, - absolute_path = absolute_path, - executable = false, - fs_stat = fs_stat, - hidden = false, - is_dot = false, - name = name, - parent = parent, - watcher = nil, - diag_status = nil, - - has_children = has_children, - group_next = nil, - link_to = link_to, - nodes = nodes, - open = open, - } - o = self:new(o) --[[@as LinkNode]] - - if is_dir_link then - o.watcher = watch.create_watcher(o) - end - - return o -end - ----Create a sanitized partial copy of a node, populating children recursively. ----@return LinkNode cloned -function LinkNode:clone() - local clone = BaseNode.clone(self) --[[@as LinkNode]] - - clone.has_children = self.has_children - clone.group_next = nil - clone.link_to = self.link_to - clone.nodes = {} - clone.open = self.open - - if self.nodes then - for _, child in ipairs(self.nodes) do - table.insert(clone.nodes, child:clone()) - end - end - - return clone -end - -return LinkNode diff --git a/lua/nvim-tree/node/root.lua b/lua/nvim-tree/node/root.lua deleted file mode 100644 index 4265d49d..00000000 --- a/lua/nvim-tree/node/root.lua +++ /dev/null @@ -1,20 +0,0 @@ -local DirectoryNode = require("nvim-tree.node.directory") - ----@class (exact) RootNode: DirectoryNode -local RootNode = DirectoryNode:new() - ----Static factory method ----@param explorer Explorer ----@param absolute_path string ----@param name string ----@param fs_stat uv.fs_stat.result|nil ----@return RootNode -function RootNode:create(explorer, absolute_path, name, fs_stat) - local o = DirectoryNode:create(explorer, nil, absolute_path, name, fs_stat) - - o = self:new(o) --[[@as RootNode]] - - return o -end - -return RootNode diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua index 2071bdab..119efc26 100644 --- a/lua/nvim-tree/renderer/builder.lua +++ b/lua/nvim-tree/renderer/builder.lua @@ -68,14 +68,14 @@ function Builder:new(opts, explorer) virtual_lines = {}, decorators = { -- priority order - DecoratorCut:create(opts, explorer), - DecoratorCopied:create(opts, explorer), - DecoratorDiagnostics:create(opts, explorer), - DecoratorBookmarks:create(opts, explorer), - DecoratorModified:create(opts, explorer), - DecoratorHidden:create(opts, explorer), - DecoratorOpened:create(opts, explorer), - DecoratorGit:create(opts, explorer), + DecoratorCut:new(opts, explorer), + DecoratorCopied:new(opts, explorer), + DecoratorDiagnostics:new(opts, explorer), + DecoratorBookmarks:new(opts, explorer), + DecoratorModified:new(opts, explorer), + DecoratorHidden:new(opts, explorer), + DecoratorOpened:new(opts, explorer), + DecoratorGit:new(opts, explorer), }, hidden_display = Builder:setup_hidden_display_function(opts), } @@ -137,7 +137,7 @@ function Builder:unwrap_highlighted_strings(highlighted_strings) end ---@private ----@param node Node +---@param node table ---@return HighlightedString icon ---@return HighlightedString name function Builder:build_folder(node) @@ -189,7 +189,7 @@ function Builder:build_symlink(node) end ---@private ----@param node Node +---@param node table ---@return HighlightedString icon ---@return HighlightedString name function Builder:build_file(node) @@ -369,7 +369,7 @@ function Builder:build_line(node, idx, num_children) self.index = self.index + 1 - node = node:last_group_node() + node = require("nvim-tree.lib").get_last_group_node(node) if node.open then self.depth = self.depth + 1 self:build_lines(node) @@ -487,7 +487,7 @@ function Builder:build() return self end ----@private +---TODO refactor back to function; this was left here to reduce PR noise ---@param opts table ---@return fun(node: Node): string|nil function Builder:setup_hidden_display_function(opts) diff --git a/lua/nvim-tree/renderer/components/diagnostics.lua b/lua/nvim-tree/renderer/components/diagnostics.lua index e51712e7..8f749343 100644 --- a/lua/nvim-tree/renderer/components/diagnostics.lua +++ b/lua/nvim-tree/renderer/components/diagnostics.lua @@ -14,7 +14,7 @@ local M = { } ---Diagnostics highlight group and position when highlight_diagnostics. ----@param node Node +---@param node table ---@return HL_POSITION position none when no status ---@return string|nil group only when status function M.get_highlight(node) @@ -38,7 +38,7 @@ function M.get_highlight(node) end ---diagnostics icon if there is a status ----@param node Node +---@param node table ---@return HighlightedString|nil modified icon function M.get_icon(node) if node and M.config.diagnostics.enable and M.config.renderer.icons.show.diagnostics then diff --git a/lua/nvim-tree/renderer/components/padding.lua b/lua/nvim-tree/renderer/components/padding.lua index 8ca25e8a..2d808cc6 100644 --- a/lua/nvim-tree/renderer/components/padding.lua +++ b/lua/nvim-tree/renderer/components/padding.lua @@ -59,7 +59,7 @@ end ---@param depth integer ---@param idx integer ---@param nodes_number integer ----@param node Node +---@param node table ---@param markers table ---@return HighlightedString[] function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop) @@ -79,7 +79,7 @@ function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_sto return { str = str, hl = { "NvimTreeIndentMarker" } } end ----@param node Node +---@param node table ---@return HighlightedString[]|nil function M.get_arrows(node) if not M.config.icons.show.folder_arrow then diff --git a/lua/nvim-tree/renderer/decorator/bookmarks.lua b/lua/nvim-tree/renderer/decorator/bookmarks.lua index 6b33970f..63138e00 100644 --- a/lua/nvim-tree/renderer/decorator/bookmarks.lua +++ b/lua/nvim-tree/renderer/decorator/bookmarks.lua @@ -4,22 +4,20 @@ local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") ---@class (exact) DecoratorBookmarks: Decorator ----@field icon HighlightedString? +---@field icon HighlightedString local DecoratorBookmarks = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorBookmarks -function DecoratorBookmarks:create(opts, explorer) - ---@type DecoratorBookmarks - local o = { +function DecoratorBookmarks:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = true, hl_pos = HL_POSITION[opts.renderer.highlight_bookmarks] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.bookmarks_placement] or ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorBookmarks]] + }) + ---@cast o DecoratorBookmarks if opts.renderer.icons.show.bookmarks then o.icon = { diff --git a/lua/nvim-tree/renderer/decorator/copied.lua b/lua/nvim-tree/renderer/decorator/copied.lua index 0debcc63..b6c4cf5e 100644 --- a/lua/nvim-tree/renderer/decorator/copied.lua +++ b/lua/nvim-tree/renderer/decorator/copied.lua @@ -4,22 +4,21 @@ local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") ---@class (exact) DecoratorCopied: Decorator ----@field icon HighlightedString? +---@field enabled boolean +---@field icon HighlightedString|nil local DecoratorCopied = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorCopied -function DecoratorCopied:create(opts, explorer) - ---@type DecoratorCopied - local o = { +function DecoratorCopied:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = true, hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none, icon_placement = ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorCopied]] + }) + ---@cast o DecoratorCopied return o end diff --git a/lua/nvim-tree/renderer/decorator/cut.lua b/lua/nvim-tree/renderer/decorator/cut.lua index b81642f6..17c69a7f 100644 --- a/lua/nvim-tree/renderer/decorator/cut.lua +++ b/lua/nvim-tree/renderer/decorator/cut.lua @@ -4,21 +4,21 @@ local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") ---@class (exact) DecoratorCut: Decorator +---@field enabled boolean +---@field icon HighlightedString|nil local DecoratorCut = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorCut -function DecoratorCut:create(opts, explorer) - ---@type DecoratorCut - local o = { +function DecoratorCut:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = true, hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none, icon_placement = ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorCut]] + }) + ---@cast o DecoratorCut return o end diff --git a/lua/nvim-tree/renderer/decorator/diagnostics.lua b/lua/nvim-tree/renderer/decorator/diagnostics.lua index 3daee7bc..bf01533e 100644 --- a/lua/nvim-tree/renderer/decorator/diagnostics.lua +++ b/lua/nvim-tree/renderer/decorator/diagnostics.lua @@ -33,22 +33,20 @@ local ICON_KEYS = { } ---@class (exact) DecoratorDiagnostics: Decorator ----@field icons HighlightedString[]? +---@field icons HighlightedString[] local DecoratorDiagnostics = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorDiagnostics -function DecoratorDiagnostics:create(opts, explorer) - ---@type DecoratorDiagnostics - local o = { +function DecoratorDiagnostics:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = opts.diagnostics.enable, hl_pos = HL_POSITION[opts.renderer.highlight_diagnostics] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.diagnostics_placement] or ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorDiagnostics]] + }) + ---@cast o DecoratorDiagnostics if not o.enabled then return o diff --git a/lua/nvim-tree/renderer/decorator/git.lua b/lua/nvim-tree/renderer/decorator/git.lua index af2c8cca..cd3f9bb8 100644 --- a/lua/nvim-tree/renderer/decorator/git.lua +++ b/lua/nvim-tree/renderer/decorator/git.lua @@ -1,4 +1,5 @@ local notify = require("nvim-tree.notify") +local explorer_node = require("nvim-tree.explorer.node") local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT @@ -9,25 +10,23 @@ local Decorator = require("nvim-tree.renderer.decorator") ---@field ord number decreasing priority ---@class (exact) DecoratorGit: Decorator ----@field file_hl table? by porcelain status e.g. "AM" ----@field folder_hl table? by porcelain status ----@field icons_by_status HighlightedStringGit[]? by human status ----@field icons_by_xy table? by porcelain status +---@field file_hl table by porcelain status e.g. "AM" +---@field folder_hl table by porcelain status +---@field icons_by_status HighlightedStringGit[] by human status +---@field icons_by_xy table by porcelain status local DecoratorGit = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorGit -function DecoratorGit:create(opts, explorer) - ---@type DecoratorGit - local o = { +function DecoratorGit:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = opts.git.enable, hl_pos = HL_POSITION[opts.renderer.highlight_git] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.git_placement] or ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorGit]] + }) + ---@cast o DecoratorGit if not o.enabled then return o @@ -148,7 +147,7 @@ function DecoratorGit:calculate_icons(node) return nil end - local git_status = node:get_git_status() + local git_status = explorer_node.get_git_status(node) if git_status == nil then return nil end @@ -209,7 +208,7 @@ function DecoratorGit:calculate_highlight(node) return nil end - local git_status = node:get_git_status() + local git_status = explorer_node.get_git_status(node) if not git_status then return nil end diff --git a/lua/nvim-tree/renderer/decorator/hidden.lua b/lua/nvim-tree/renderer/decorator/hidden.lua index 1df68c48..d6c125ae 100644 --- a/lua/nvim-tree/renderer/decorator/hidden.lua +++ b/lua/nvim-tree/renderer/decorator/hidden.lua @@ -1,24 +1,23 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT +local explorer_node = require("nvim-tree.explorer.node") local Decorator = require("nvim-tree.renderer.decorator") ---@class (exact) DecoratorHidden: Decorator ----@field icon HighlightedString? +---@field icon HighlightedString|nil local DecoratorHidden = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorHidden -function DecoratorHidden:create(opts, explorer) - ---@type DecoratorHidden - local o = { +function DecoratorHidden:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = true, hl_pos = HL_POSITION[opts.renderer.highlight_hidden] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.hidden_placement] or ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorHidden]] + }) + ---@cast o DecoratorHidden if opts.renderer.icons.show.hidden then o.icon = { @@ -35,7 +34,7 @@ end ---@param node Node ---@return HighlightedString[]|nil icons function DecoratorHidden:calculate_icons(node) - if self.enabled and node:is_dotfile() then + if self.enabled and explorer_node.is_dotfile(node) then return { self.icon } end end @@ -44,7 +43,7 @@ end ---@param node Node ---@return string|nil group function DecoratorHidden:calculate_highlight(node) - if not self.enabled or self.hl_pos == HL_POSITION.none or not node:is_dotfile() then + if not self.enabled or self.hl_pos == HL_POSITION.none or (not explorer_node.is_dotfile(node)) then return nil end diff --git a/lua/nvim-tree/renderer/decorator/init.lua b/lua/nvim-tree/renderer/decorator/init.lua index a80ce615..92fcc579 100644 --- a/lua/nvim-tree/renderer/decorator/init.lua +++ b/lua/nvim-tree/renderer/decorator/init.lua @@ -1,8 +1,6 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT ----Abstract Decorator ----Uses the factory pattern to instantiate child instances. ---@class (exact) Decorator ---@field private __index? table ---@field protected explorer Explorer diff --git a/lua/nvim-tree/renderer/decorator/modified.lua b/lua/nvim-tree/renderer/decorator/modified.lua index 4665343f..75bb59c8 100644 --- a/lua/nvim-tree/renderer/decorator/modified.lua +++ b/lua/nvim-tree/renderer/decorator/modified.lua @@ -9,19 +9,17 @@ local Decorator = require("nvim-tree.renderer.decorator") ---@field icon HighlightedString|nil local DecoratorModified = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorModified -function DecoratorModified:create(opts, explorer) - ---@type DecoratorModified - local o = { +function DecoratorModified:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = opts.modified.enable, hl_pos = HL_POSITION[opts.renderer.highlight_modified] or HL_POSITION.none, icon_placement = ICON_PLACEMENT[opts.renderer.icons.modified_placement] or ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorModified]] + }) + ---@cast o DecoratorModified if not o.enabled then return o diff --git a/lua/nvim-tree/renderer/decorator/opened.lua b/lua/nvim-tree/renderer/decorator/opened.lua index 6f2ad58b..5a17c2da 100644 --- a/lua/nvim-tree/renderer/decorator/opened.lua +++ b/lua/nvim-tree/renderer/decorator/opened.lua @@ -6,22 +6,21 @@ local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local Decorator = require("nvim-tree.renderer.decorator") ---@class (exact) DecoratorOpened: Decorator +---@field enabled boolean ---@field icon HighlightedString|nil local DecoratorOpened = Decorator:new() ----Static factory method ---@param opts table ---@param explorer Explorer ---@return DecoratorOpened -function DecoratorOpened:create(opts, explorer) - ---@type DecoratorOpened - local o = { +function DecoratorOpened:new(opts, explorer) + local o = Decorator.new(self, { explorer = explorer, enabled = true, hl_pos = HL_POSITION[opts.renderer.highlight_opened_files] or HL_POSITION.none, icon_placement = ICON_PLACEMENT.none, - } - o = self:new(o) --[[@as DecoratorOpened]] + }) + ---@cast o DecoratorOpened return o end diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index 495e6679..0c341c9e 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -112,7 +112,8 @@ function M.find_node(nodes, fn) end) :iterate() i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1 - if node and node.explorer.live_filter.filter then + local explorer = require("nvim-tree.core").get_explorer() + if explorer and explorer.live_filter.filter then i = i + 1 end return node, i @@ -120,7 +121,7 @@ end -- Find the line number of a node. -- Return -1 is node is nil or not found. ----@param node Node? +---@param node Node|nil ---@return integer function M.find_node_line(node) if not node then @@ -173,6 +174,16 @@ function M.get_node_from_path(path) :iterate() end +---Get the highest parent of grouped nodes +---@param node Node +---@return Node node or parent +function M.get_parent_of_group(node) + while node and node.parent and node.parent.group_next do + node = node.parent + end + return node +end + M.default_format_hidden_count = function(hidden_count, simple) local parts = {} local total_count = 0 @@ -462,7 +473,7 @@ end ---Focus node passed as parameter if visible, otherwise focus first visible parent. ---If none of the parents is visible focus root. ---If node is nil do nothing. ----@param node Node? node to focus +---@param node Node|nil node to focus function M.focus_node_or_parent(node) local explorer = require("nvim-tree.core").get_explorer() @@ -538,6 +549,14 @@ function M.array_remove_nils(array) end, array) end +---@param f fun(node: Node|nil) +---@return function +function M.inject_node(f) + return function() + f(require("nvim-tree.lib").get_node_at_cursor()) + end +end + --- Is the buffer named NvimTree_[0-9]+ a tree? filetype is "NvimTree" or not readable file. --- This is cheap, as the readable test should only ever be needed when resuming a vim session. ---@param bufnr number|nil may be 0 or nil for current @@ -559,16 +578,4 @@ function M.is_nvim_tree_buf(bufnr) return false end ---- path is an executable file or directory ----@param absolute_path string ----@return boolean -function M.is_executable(absolute_path) - if M.is_windows or M.is_wsl then - --- executable detection on windows is buggy and not performant hence it is disabled - return false - else - return vim.loop.fs_access(absolute_path, "X") or false - end -end - return M