From ccf43b0a74d3b1feb28f4de28372b1e5fc65111b Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Mon, 7 Oct 2024 15:33:55 +1100 Subject: [PATCH 1/7] extract DirectoryLinkNode and FileLinkNode, move Node methods to children --- lua/nvim-tree/log.lua | 19 +++ lua/nvim-tree/node/directory-link.lua | 54 +++++++++ lua/nvim-tree/node/directory.lua | 113 ++++++++++++++++++ lua/nvim-tree/node/factory.lua | 31 +++-- lua/nvim-tree/node/file-link.lua | 50 ++++++++ lua/nvim-tree/node/file.lua | 19 ++- lua/nvim-tree/node/init.lua | 159 ++++++-------------------- lua/nvim-tree/node/link.lua | 89 -------------- 8 files changed, 311 insertions(+), 223 deletions(-) create mode 100644 lua/nvim-tree/node/directory-link.lua create mode 100644 lua/nvim-tree/node/file-link.lua delete mode 100644 lua/nvim-tree/node/link.lua diff --git a/lua/nvim-tree/log.lua b/lua/nvim-tree/log.lua index 8e796b9e6c8..ad8f34cf175 100644 --- a/lua/nvim-tree/log.lua +++ b/lua/nvim-tree/log.lua @@ -21,6 +21,25 @@ function M.raw(typ, fmt, ...) end end +--- Write to a new file +---@param typ string as per log.types config +---@param path string absolute path +---@param fmt string for string.format +---@param ... any arguments for string.format +function M.file(typ, path, fmt, ...) + if not M.enabled(typ) then + return + end + + local line = string.format(fmt, ...) + local file = io.open(path, "w") + if file then + io.output(file) + io.write(line) + io.close(file) + end +end + ---@class Profile ---@field start number nanos ---@field tag string diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua new file mode 100644 index 00000000000..eedd6ac080d --- /dev/null +++ b/lua/nvim-tree/node/directory-link.lua @@ -0,0 +1,54 @@ +local git = require("nvim-tree.git") + +local DirectoryNode = require("nvim-tree.node.directory") + +---@class (exact) DirectoryLinkNode: DirectoryNode +---@field link_to string absolute path +---@field fs_stat_target uv.fs_stat.result +local DirectoryLinkNode = DirectoryNode:new() + +---Static factory method +---@param explorer Explorer +---@param parent Node +---@param absolute_path string +---@param link_to string +---@param name string +---@param fs_stat uv.fs_stat.result? +---@param fs_stat_target uv.fs_stat.result +---@return DirectoryLinkNode? nil on vim.loop.fs_realpath failure +function DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name, fs_stat, fs_stat_target) + -- create DirectoryNode with the target path for the watcher + local o = DirectoryNode:create(explorer, parent, link_to, name, fs_stat) + + o = self:new(o) --[[@as DirectoryLinkNode]] + + -- reset absolute path to the link itself + o.absolute_path = absolute_path + + o.type = "link" + o.link_to = link_to + o.fs_stat_target = fs_stat_target + + return o +end + +-----Update the GitStatus of link target +-----@param parent_ignored boolean +-----@param status table|nil +function DirectoryLinkNode:update_git_status(parent_ignored, status) + self.git_status = git.git_status_dir(parent_ignored, status, self.link_to) +end + +---Create a sanitized partial copy of a node, populating children recursively. +---@return DirectoryLinkNode cloned +function DirectoryLinkNode:clone() + local clone = DirectoryNode.clone(self) --[[@as DirectoryLinkNode]] + + clone.type = self.type + clone.link_to = self.link_to + clone.fs_stat_target = self.fs_stat_target + + return clone +end + +return DirectoryLinkNode diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua index 050d3e35b8f..c05f82f2c43 100644 --- a/lua/nvim-tree/node/directory.lua +++ b/lua/nvim-tree/node/directory.lua @@ -1,3 +1,4 @@ +local git = require("nvim-tree.git") local watch = require("nvim-tree.explorer.watch") local BaseNode = require("nvim-tree.node") @@ -58,6 +59,118 @@ function DirectoryNode:destroy() end end +---@return boolean +function DirectoryNode: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 + +-- If node is grouped, return the last node in the group. Otherwise, return the given node. +---@return Node +function DirectoryNode:last_group_node() + local node = self --[[@as BaseNode]] + + while node.group_next do + node = node.group_next + end + + return node +end + +---Group empty folders +-- Recursively group nodes +---@return Node[] +function DirectoryNode: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 + 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 DirectoryNode: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 + +---Update the GitStatus of absolute path of the directory +---@param parent_ignored boolean +---@param status table|nil +function DirectoryNode:update_git_status(parent_ignored, status) + self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path) +end + +---@return GitStatus|nil +function BaseNode:get_git_status() + if not self.git_status or 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 DirectoryNode: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 + ---Create a sanitized partial copy of a node, populating children recursively. ---@return DirectoryNode cloned function DirectoryNode:clone() diff --git a/lua/nvim-tree/node/factory.lua b/lua/nvim-tree/node/factory.lua index a46057da111..e6a7e7b437a 100644 --- a/lua/nvim-tree/node/factory.lua +++ b/lua/nvim-tree/node/factory.lua @@ -1,5 +1,6 @@ +local DirectoryLinkNode = require("nvim-tree.node.directory-link") local DirectoryNode = require("nvim-tree.node.directory") -local LinkNode = require("nvim-tree.node.link") +local FileLinkNode = require("nvim-tree.node.file-link") local FileNode = require("nvim-tree.node.file") local Watcher = require("nvim-tree.watcher") @@ -8,21 +9,37 @@ local M = {} ---Factory function to create the appropriate Node ---@param explorer Explorer ---@param parent Node ----@param abs string +---@param absolute_path 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) +function M.create_node(explorer, parent, absolute_path, 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) + if stat.type == "directory" then + -- directory must be readable and enumerable + if vim.loop.fs_access(absolute_path, "R") and Watcher.is_fs_event_capable(absolute_path) then + return DirectoryNode:create(explorer, parent, absolute_path, name, stat) + end elseif stat.type == "file" then - return FileNode:create(explorer, parent, abs, name, stat) + -- any file + return FileNode:create(explorer, parent, absolute_path, name, stat) elseif stat.type == "link" then - return LinkNode:create(explorer, parent, abs, name, stat) + -- link target path and stat must resolve + local link_to = vim.loop.fs_realpath(absolute_path) + local link_to_stat = link_to and vim.loop.fs_stat(link_to) + if not link_to or not link_to_stat then + return + end + + -- choose directory or file + if link_to_stat.type == "directory" then + return DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name, stat, link_to_stat) + else + return FileLinkNode:create(explorer, parent, absolute_path, link_to, name, stat, link_to_stat) + end end return nil diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua new file mode 100644 index 00000000000..64ac391030a --- /dev/null +++ b/lua/nvim-tree/node/file-link.lua @@ -0,0 +1,50 @@ +local git = require("nvim-tree.git") + +local FileNode = require("nvim-tree.node.file") + +---@class (exact) FileLinkNode: FileNode +---@field link_to string absolute path +---@field fs_stat_target uv.fs_stat.result +local FileLinkNode = FileNode:new() + +---Static factory method +---@param explorer Explorer +---@param parent Node +---@param absolute_path string +---@param link_to string +---@param name string +---@param fs_stat uv.fs_stat.result? +---@param fs_stat_target uv.fs_stat.result +---@return FileLinkNode? nil on vim.loop.fs_realpath failure +function FileLinkNode:create(explorer, parent, absolute_path, link_to, name, fs_stat, fs_stat_target) + local o = FileNode:create(explorer, parent, absolute_path, name, fs_stat) + + o = self:new(o) --[[@as FileLinkNode]] + + o.type = "link" + o.link_to = link_to + o.fs_stat_target = fs_stat_target + + return o +end + +-----Update the GitStatus of link target +-----@param parent_ignored boolean +-----@param status table|nil +function FileLinkNode:update_git_status(parent_ignored, status) + self.git_status = git.git_status_file(parent_ignored, status, self.link_to) +end + +---Create a sanitized partial copy of a node +---@return FileLinkNode cloned +function FileLinkNode:clone() + local clone = FileNode.clone(self) --[[@as FileLinkNode]] + + clone.type = self.type + clone.link_to = self.link_to + clone.fs_stat_target = self.fs_stat_target + + return clone +end + +return FileLinkNode diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua index f504631b7fb..547a83bd0ff 100644 --- a/lua/nvim-tree/node/file.lua +++ b/lua/nvim-tree/node/file.lua @@ -1,3 +1,4 @@ +local git = require("nvim-tree.git") local utils = require("nvim-tree.utils") local BaseNode = require("nvim-tree.node") @@ -36,7 +37,23 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat) return o end ----Create a sanitized partial copy of a node, populating children recursively. +---Update the GitStatus of absolute path of the file +---@param parent_ignored boolean +---@param status table|nil +function FileNode:update_git_status(parent_ignored, status) + self.git_status = git.git_status_file(parent_ignored, status, self.absolute_path) +end + +---@return GitStatus|nil +function FileNode:get_git_status() + if not self.git_status then + return nil + end + + return self.git_status.file and { self.git_status.file } +end + +---Create a sanitized partial copy of a node ---@return FileNode cloned function FileNode:clone() local clone = BaseNode.clone(self) --[[@as FileNode]] diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua index b915df7df1f..bd49bf3bed9 100644 --- a/lua/nvim-tree/node/init.lua +++ b/lua/nvim-tree/node/init.lua @@ -1,5 +1,12 @@ local git = require("nvim-tree.git") +---TODO remove all references to directory fields: +------@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 + ---Abstract Node class. ---Uses the abstract factory pattern to instantiate child instances. ---@class (exact) BaseNode @@ -18,7 +25,7 @@ local git = require("nvim-tree.git") ---@field diag_status DiagStatus? local BaseNode = {} ----@alias Node RootNode|BaseNode|DirectoryNode|FileNode|LinkNode +---@alias Node RootNode|BaseNode|DirectoryNode|FileNode|DirectoryLinkNode|FileLinkNode ---@param o BaseNode? ---@return BaseNode @@ -57,99 +64,38 @@ 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 + return 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 +-- If node is grouped, return the last node in the group. Otherwise, return the given node. +---@return Node +function BaseNode:last_group_node() + return self end ----@return GitStatus|nil -function BaseNode:get_git_status() - if not self.git_status then - -- status doesn't exist - return nil - end +---Group empty folders +-- Recursively group nodes +---@return Node[] +function BaseNode:group_empty_folders() + return {} +end - if not self.nodes then - -- file - return self.git_status.file and { self.git_status.file } - end +---Ungroup empty folders +function BaseNode:ungroup_empty_folders() +end - -- dir - if not self.explorer.opts.git.show_on_dirs then - return nil - end +---Update the GitStatus of the node +---@param parent_ignored boolean +---@param status table? +function BaseNode:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local +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 +---@return GitStatus? +function BaseNode:get_git_status() 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 +---@param _ table projects +function BaseNode:reload_node_status(_) end ---@return boolean @@ -170,20 +116,8 @@ function BaseNode:is_dotfile() 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 +---@param project table? +---@param root string? function BaseNode:update_parent_statuses(project, root) local node = self while project and node do @@ -262,33 +196,6 @@ function BaseNode:toggle_group_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 diff --git a/lua/nvim-tree/node/link.lua b/lua/nvim-tree/node/link.lua deleted file mode 100644 index 2df9d920092..00000000000 --- 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 From d4242af40c98d339aa3857383b9c135a8ecae5f3 Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Mon, 7 Oct 2024 16:35:04 +1100 Subject: [PATCH 2/7] temporarily move DirectoryNode methods into BaseNode for easier reviewing --- lua/nvim-tree/node/directory.lua | 107 +-------------------- lua/nvim-tree/node/init.lua | 160 ++++++++++++++++++++++++++----- 2 files changed, 140 insertions(+), 127 deletions(-) diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua index c05f82f2c43..fba592fb7e5 100644 --- a/lua/nvim-tree/node/directory.lua +++ b/lua/nvim-tree/node/directory.lua @@ -9,7 +9,8 @@ local BaseNode = require("nvim-tree.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() +local DirectoryNode = BaseNode.dn +-- local DirectoryNode = BaseNode:new() ---Static factory method ---@param explorer Explorer @@ -59,49 +60,6 @@ function DirectoryNode:destroy() end end ----@return boolean -function DirectoryNode: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 - --- If node is grouped, return the last node in the group. Otherwise, return the given node. ----@return Node -function DirectoryNode:last_group_node() - local node = self --[[@as BaseNode]] - - while node.group_next do - node = node.group_next - end - - return node -end - ----Group empty folders --- Recursively group nodes ----@return Node[] -function DirectoryNode: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 - 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 DirectoryNode: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 - ---Update the GitStatus of absolute path of the directory ---@param parent_ignored boolean ---@param status table|nil @@ -109,67 +67,6 @@ function DirectoryNode:update_git_status(parent_ignored, status) self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path) end ----@return GitStatus|nil -function BaseNode:get_git_status() - if not self.git_status or 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 DirectoryNode: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 ---Create a sanitized partial copy of a node, populating children recursively. ---@return DirectoryNode cloned diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua index bd49bf3bed9..a36aca7a0e4 100644 --- a/lua/nvim-tree/node/init.lua +++ b/lua/nvim-tree/node/init.lua @@ -1,5 +1,6 @@ local git = require("nvim-tree.git") +---TODO remove all @cast, @as ---TODO remove all references to directory fields: ------@field has_children boolean ------@field group_next Node? -- If node is grouped, this points to the next child dir/link node @@ -38,6 +39,11 @@ function BaseNode:new(o) return o end +-- TODO temporary hack to allow DirectoryNode methods in this file, for easier reviewing +---@class DirectoryNode +local DirectoryNode = BaseNode:new() +BaseNode.dn = DirectoryNode ---@diagnostic disable-line: inject-field + function BaseNode:destroy() if self.watcher then self.watcher:destroy() @@ -63,25 +69,8 @@ function BaseNode:is(T) end ---@return boolean -function BaseNode:has_one_child_folder() - 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() - return self -end - ----Group empty folders --- Recursively group nodes ----@return Node[] -function BaseNode:group_empty_folders() - return {} -end - ----Ungroup empty folders -function BaseNode:ungroup_empty_folders() +function DirectoryNode: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 ---Update the GitStatus of the node @@ -94,8 +83,66 @@ end function BaseNode:get_git_status() end ----@param _ table projects -function BaseNode:reload_node_status(_) +---@return GitStatus|nil +function DirectoryNode:get_git_status() + if not self.git_status or 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 DirectoryNode: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 @@ -116,6 +163,18 @@ function BaseNode:is_dotfile() return false end +-- If node is grouped, return the last node in the group. Otherwise, return the given node. +---@return Node +function DirectoryNode:last_group_node() + local node = self --[[@as BaseNode]] + + while node.group_next do + node = node.group_next + end + + return node +end + ---@param project table? ---@param root string? function BaseNode:update_parent_statuses(project, root) @@ -186,7 +245,7 @@ function BaseNode:get_all_nodes_in_group() end -- Toggle group empty folders -function BaseNode:toggle_group_folders() +function DirectoryNode:toggle_group_folders() local is_grouped = self.group_next ~= nil if is_grouped then @@ -196,6 +255,32 @@ function BaseNode:toggle_group_folders() end end +---Group empty folders +-- Recursively group nodes +---@return Node[] +function DirectoryNode: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 + 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 DirectoryNode:ungroup_empty_folders() + local cur = self --[[@as DirectoryNode]] + while cur and cur.group_next do + cur.nodes = { cur.group_next } + cur.group_next = nil + cur = cur.nodes[1] --[[@as DirectoryNode]] + end +end + function BaseNode:expand_or_collapse(toggle_group) toggle_group = toggle_group or false if self.has_children then @@ -208,6 +293,7 @@ function BaseNode:expand_or_collapse(toggle_group) end local head_node = self:get_parent_of_group() + ---@cast head_node DirectoryNode -- TODO move this to the class if toggle_group then head_node:toggle_group_folders() end @@ -251,4 +337,34 @@ function BaseNode:clone() return clone end +-- +-- TODO temporary hack to allow DirectoryNode methods in this file, for easier reviewing +-- + +---@return boolean +function BaseNode:has_one_child_folder() + return false +end + +---@param _ table projects +function BaseNode:reload_node_status(_) +end + +-- If node is grouped, return the last node in the group. Otherwise, return the given node. +---@return Node +function BaseNode:last_group_node() + return self +end + +---Group empty folders +-- Recursively group nodes +---@return Node[] +function BaseNode:group_empty_folders() + return {} +end + +---Ungroup empty folders +function BaseNode:ungroup_empty_folders() +end + return BaseNode From ebe5cca5e0906ae59f90ec85d9fca7fd6e9e7e47 Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Mon, 7 Oct 2024 16:59:45 +1100 Subject: [PATCH 3/7] move mostly unchanged DirectoryNode methods back to BaseNode --- lua/nvim-tree/node/directory.lua | 52 +++++++++++++++- lua/nvim-tree/node/init.lua | 102 +++---------------------------- 2 files changed, 59 insertions(+), 95 deletions(-) diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua index fba592fb7e5..ada8b31ed90 100644 --- a/lua/nvim-tree/node/directory.lua +++ b/lua/nvim-tree/node/directory.lua @@ -9,8 +9,7 @@ local BaseNode = require("nvim-tree.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.dn --- local DirectoryNode = BaseNode:new() +local DirectoryNode = BaseNode:new() ---Static factory method ---@param explorer Explorer @@ -67,6 +66,55 @@ function DirectoryNode:update_git_status(parent_ignored, status) self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path) end +---@return GitStatus|nil +function DirectoryNode:get_git_status() + if not self.git_status or 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 ---Create a sanitized partial copy of a node, populating children recursively. ---@return DirectoryNode cloned diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua index a36aca7a0e4..4d33e74b1b5 100644 --- a/lua/nvim-tree/node/init.lua +++ b/lua/nvim-tree/node/init.lua @@ -39,11 +39,6 @@ function BaseNode:new(o) return o end --- TODO temporary hack to allow DirectoryNode methods in this file, for easier reviewing ----@class DirectoryNode -local DirectoryNode = BaseNode:new() -BaseNode.dn = DirectoryNode ---@diagnostic disable-line: inject-field - function BaseNode:destroy() if self.watcher then self.watcher:destroy() @@ -69,7 +64,7 @@ function BaseNode:is(T) end ---@return boolean -function DirectoryNode:has_one_child_folder() +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 @@ -83,58 +78,8 @@ end function BaseNode:get_git_status() end ----@return GitStatus|nil -function DirectoryNode:get_git_status() - if not self.git_status or 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 DirectoryNode:reload_node_status(projects) +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 @@ -165,7 +110,7 @@ end -- If node is grouped, return the last node in the group. Otherwise, return the given node. ---@return Node -function DirectoryNode:last_group_node() +function BaseNode:last_group_node() local node = self --[[@as BaseNode]] while node.group_next do @@ -245,7 +190,7 @@ function BaseNode:get_all_nodes_in_group() end -- Toggle group empty folders -function DirectoryNode:toggle_group_folders() +function BaseNode:toggle_group_folders() local is_grouped = self.group_next ~= nil if is_grouped then @@ -258,10 +203,11 @@ end ---Group empty folders -- Recursively group nodes ---@return Node[] -function DirectoryNode:group_empty_folders() +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 {} @@ -272,12 +218,12 @@ 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 DirectoryNode:ungroup_empty_folders() - local cur = self --[[@as DirectoryNode]] +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] --[[@as DirectoryNode]] + cur = cur.nodes[1] end end @@ -337,34 +283,4 @@ function BaseNode:clone() return clone end --- --- TODO temporary hack to allow DirectoryNode methods in this file, for easier reviewing --- - ----@return boolean -function BaseNode:has_one_child_folder() - return false -end - ----@param _ table projects -function BaseNode:reload_node_status(_) -end - --- If node is grouped, return the last node in the group. Otherwise, return the given node. ----@return Node -function BaseNode:last_group_node() - return self -end - ----Group empty folders --- Recursively group nodes ----@return Node[] -function BaseNode:group_empty_folders() - return {} -end - ----Ungroup empty folders -function BaseNode:ungroup_empty_folders() -end - return BaseNode From cf17675702a88d66f024654d09e6234a39e56623 Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Mon, 7 Oct 2024 17:03:33 +1100 Subject: [PATCH 4/7] tidy --- lua/nvim-tree/node/init.lua | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua index 4d33e74b1b5..9732c9e4581 100644 --- a/lua/nvim-tree/node/init.lua +++ b/lua/nvim-tree/node/init.lua @@ -1,12 +1,7 @@ local git = require("nvim-tree.git") ----TODO remove all @cast, @as +---TODO remove all @cast ---TODO remove all references to directory fields: -------@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 ---Abstract Node class. ---Uses the abstract factory pattern to instantiate child instances. @@ -111,7 +106,8 @@ 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]] + local node = self + --- @cast node BaseNode while node.group_next do node = node.group_next @@ -239,7 +235,6 @@ function BaseNode:expand_or_collapse(toggle_group) end local head_node = self:get_parent_of_group() - ---@cast head_node DirectoryNode -- TODO move this to the class if toggle_group then head_node:toggle_group_folders() end From 5851dec163414c7615ae05aadce2750fe0822c91 Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Fri, 11 Oct 2024 14:42:51 +1100 Subject: [PATCH 5/7] git.git_status_file takes an array --- lua/nvim-tree/git/init.lua | 23 +++++++++++++++++++---- lua/nvim-tree/node/file-link.lua | 4 ++-- lua/nvim-tree/node/file.lua | 4 ++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index b963cfd7d3e..13ef110b631 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -303,13 +303,28 @@ function M.git_status_dir(parent_ignored, status, absolute_path) end end +---Git file status for an absolute path ---@param parent_ignored boolean ---@param status table|nil ----@param absolute_path string +---@param absolute_paths string[] status for first match is returned ---@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 } +function M.git_status_file(parent_ignored, status, absolute_paths) + if parent_ignored then + return { file = "!!" } + end + + if not status or not status.files then + return {} + end + + for _, p in ipairs(absolute_paths) do + local s = status.files[p] + if s then + return { file = s } + end + end + + return {} end function M.purge_state() diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua index 64ac391030a..a51e275999d 100644 --- a/lua/nvim-tree/node/file-link.lua +++ b/lua/nvim-tree/node/file-link.lua @@ -28,11 +28,11 @@ function FileLinkNode:create(explorer, parent, absolute_path, link_to, name, fs_ return o end ------Update the GitStatus of link target +-----Update the GitStatus of the target otherwise the link itself -----@param parent_ignored boolean -----@param status table|nil function FileLinkNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_file(parent_ignored, status, self.link_to) + self.git_status = git.git_status_file(parent_ignored, status, { self.link_to, self.absolute_path, }) end ---Create a sanitized partial copy of a node diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua index 547a83bd0ff..0bdbd4e236c 100644 --- a/lua/nvim-tree/node/file.lua +++ b/lua/nvim-tree/node/file.lua @@ -37,11 +37,11 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat) return o end ----Update the GitStatus of absolute path of the file +---Update the GitStatus of the file ---@param parent_ignored boolean ---@param status table|nil function FileNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_file(parent_ignored, status, self.absolute_path) + self.git_status = git.git_status_file(parent_ignored, status, { self.absolute_path }) end ---@return GitStatus|nil From a965c65461ebf9437f71ac51b470345ada747ff8 Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Fri, 11 Oct 2024 15:29:05 +1100 Subject: [PATCH 6/7] update git status of links --- lua/nvim-tree/git/init.lua | 30 +++++++++++++-------------- lua/nvim-tree/node/directory-link.lua | 4 ++-- lua/nvim-tree/node/directory.lua | 4 ++-- lua/nvim-tree/node/file-link.lua | 2 +- lua/nvim-tree/node/file.lua | 2 +- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua index 13ef110b631..52f05d14b4a 100644 --- a/lua/nvim-tree/git/init.lua +++ b/lua/nvim-tree/git/init.lua @@ -283,32 +283,35 @@ function M.load_project_status(path) end end +---Git file and directory status for an absolute path with optional file fallback ---@param parent_ignored boolean ---@param status table|nil ----@param absolute_path string +---@param path string +---@param path_file string? alternative file path when no other file status ---@return GitStatus|nil -function M.git_status_dir(parent_ignored, status, absolute_path) +function M.git_status_dir(parent_ignored, status, path, path_file) if parent_ignored then return { file = "!!" } end if status then return { - file = status.files and status.files[absolute_path], + file = status.files and status.files[path] or path_file and status.files[path_file], dir = status.dirs and { - direct = status.dirs.direct[absolute_path], - indirect = status.dirs.indirect[absolute_path], + direct = status.dirs.direct[path], + indirect = status.dirs.indirect[path], }, } end end ----Git file status for an absolute path +---Git file status for an absolute path with optional fallback ---@param parent_ignored boolean ---@param status table|nil ----@param absolute_paths string[] status for first match is returned +---@param path string +---@param path_fallback string? ---@return GitStatus -function M.git_status_file(parent_ignored, status, absolute_paths) +function M.git_status_file(parent_ignored, status, path, path_fallback) if parent_ignored then return { file = "!!" } end @@ -317,14 +320,9 @@ function M.git_status_file(parent_ignored, status, absolute_paths) return {} end - for _, p in ipairs(absolute_paths) do - local s = status.files[p] - if s then - return { file = s } - end - end - - return {} + return { + file = status.files[path] or status.files[path_fallback] + } end function M.purge_state() diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua index eedd6ac080d..c8759541e22 100644 --- a/lua/nvim-tree/node/directory-link.lua +++ b/lua/nvim-tree/node/directory-link.lua @@ -32,11 +32,11 @@ function DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name return o end ------Update the GitStatus of link target +-----Update the directory GitStatus of link target and the file status of the link itself -----@param parent_ignored boolean -----@param status table|nil function DirectoryLinkNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_dir(parent_ignored, status, self.link_to) + self.git_status = git.git_status_dir(parent_ignored, status, self.link_to, self.absolute_path) end ---Create a sanitized partial copy of a node, populating children recursively. diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua index ada8b31ed90..4bb216b7518 100644 --- a/lua/nvim-tree/node/directory.lua +++ b/lua/nvim-tree/node/directory.lua @@ -59,11 +59,11 @@ function DirectoryNode:destroy() end end ----Update the GitStatus of absolute path of the directory +---Update the GitStatus of the directory ---@param parent_ignored boolean ---@param status table|nil function DirectoryNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path) + self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path, nil) end ---@return GitStatus|nil diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua index a51e275999d..60c50d771f9 100644 --- a/lua/nvim-tree/node/file-link.lua +++ b/lua/nvim-tree/node/file-link.lua @@ -32,7 +32,7 @@ end -----@param parent_ignored boolean -----@param status table|nil function FileLinkNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_file(parent_ignored, status, { self.link_to, self.absolute_path, }) + self.git_status = git.git_status_file(parent_ignored, status, self.link_to, self.absolute_path) end ---Create a sanitized partial copy of a node diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua index 0bdbd4e236c..cbf3f96df5a 100644 --- a/lua/nvim-tree/node/file.lua +++ b/lua/nvim-tree/node/file.lua @@ -41,7 +41,7 @@ end ---@param parent_ignored boolean ---@param status table|nil function FileNode:update_git_status(parent_ignored, status) - self.git_status = git.git_status_file(parent_ignored, status, { self.absolute_path }) + self.git_status = git.git_status_file(parent_ignored, status, self.absolute_path, nil) end ---@return GitStatus|nil From d300f37246c4e420328589e8d5bc9ee1bd6613ef Mon Sep 17 00:00:00 2001 From: Alexander Courtis Date: Fri, 11 Oct 2024 15:37:44 +1100 Subject: [PATCH 7/7] luacheck hack --- lua/nvim-tree/node/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua index 1bd404a6f2a..6cebda0b49b 100644 --- a/lua/nvim-tree/node/init.lua +++ b/lua/nvim-tree/node/init.lua @@ -63,11 +63,13 @@ 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 +--luacheck: push ignore 212 ---Update the GitStatus of the node ---@param parent_ignored boolean ---@param status table? function BaseNode:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local end +--luacheck: pop ---@return GitStatus? function BaseNode:get_git_status()