From 8859bbb3b8436c118f6f69994238ddec1d0ec567 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 11 Oct 2024 14:07:48 +1100
Subject: [PATCH 01/88] add todo

---
 lua/nvim-tree/node/init.lua | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index d5e78678545..06635465232 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -1,5 +1,8 @@
 local git = require("nvim-tree.git")
 
+---TODO remove all @cast
+---TODO remove all references to directory fields:
+
 ---Abstract Node class.
 ---Uses the abstract factory pattern to instantiate child instances.
 ---@class (exact) BaseNode

From 98ca98cd87acf4c699607cda76edda28d1220076 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 11 Oct 2024 17:49:34 +1100
Subject: [PATCH 02/88] refactor(#2886): multi instance: node class
 refactoring: extract links, *_git_status (#2944)

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack
---
 lua/nvim-tree/git/init.lua            | 31 +++++++---
 lua/nvim-tree/log.lua                 | 19 ++++++
 lua/nvim-tree/node/directory-link.lua | 54 ++++++++++++++++
 lua/nvim-tree/node/directory.lua      | 58 +++++++++++++++++
 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           | 89 ++++-----------------------
 lua/nvim-tree/node/link.lua           | 89 ---------------------------
 9 files changed, 256 insertions(+), 184 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/git/init.lua b/lua/nvim-tree/git/init.lua
index 9e828bd8546..b4fb55314d4 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -283,33 +283,46 @@ 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 with optional fallback
 ---@param parent_ignored boolean
 ---@param status table|nil
----@param absolute_path string
+---@param path string
+---@param path_fallback 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 }
+function M.git_status_file(parent_ignored, status, path, path_fallback)
+  if parent_ignored then
+    return { file = "!!" }
+  end
+
+  if not status or not status.files then
+    return {}
+  end
+
+  return {
+    file = status.files[path] or status.files[path_fallback]
+  }
 end
 
 function M.purge_state()
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..c8759541e22
--- /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 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.absolute_path)
+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..4bb216b7518 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,63 @@ function DirectoryNode:destroy()
   end
 end
 
+---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, nil)
+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
 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..60c50d771f9
--- /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 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.absolute_path)
+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..cbf3f96df5a 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 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, nil)
+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 06635465232..086177fcf85 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -21,7 +21,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
@@ -63,84 +63,16 @@ 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|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
+---@param status table?
+function BaseNode:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local
 end
+--luacheck: pop
 
----@return GitStatus|nil
+---@return GitStatus?
 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
@@ -176,7 +108,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
@@ -185,8 +118,8 @@ function BaseNode:last_group_node()
   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
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 893957a8d95706856cc531bcd31b6ea8da088f1d Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 11 Oct 2024 18:00:25 +1100
Subject: [PATCH 03/88] safer git_status_dir

---
 lua/nvim-tree/git/init.lua | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index b4fb55314d4..b16cc58320f 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -296,10 +296,10 @@ function M.git_status_dir(parent_ignored, status, path, path_file)
 
   if status then
     return {
-      file = status.files and status.files[path] or path_file and status.files[path_file],
+      file = status.files and (status.files[path] or status.files[path_file]),
       dir = status.dirs and {
-        direct = status.dirs.direct[path],
-        indirect = status.dirs.indirect[path],
+        direct = status.dirs.direct and status.dirs.direct[path],
+        indirect = status.dirs.indirect and status.dirs.indirect[path],
       },
     }
   end

From 03f9dd29c4326723f649364714bff22755e13c76 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 14 Oct 2024 10:50:22 +1100
Subject: [PATCH 04/88] refactor(#2886): multi instance: node class
 refactoring: DirectoryNode:expand_or_collapse (#2957)

move expand_or_collapse to DirectoryNode
---
 lua/nvim-tree/actions/moves/item.lua |  9 +++++++--
 lua/nvim-tree/api.lua                |  5 ++++-
 lua/nvim-tree/node/directory.lua     | 29 +++++++++++++++++++++++++++
 lua/nvim-tree/node/init.lua          | 30 ----------------------------
 4 files changed, 40 insertions(+), 33 deletions(-)

diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index ec73d010b5f..a327f0b0bef 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -4,6 +4,8 @@ local core = require("nvim-tree.core")
 local lib = require("nvim-tree.lib")
 local diagnostics = require("nvim-tree.diagnostics")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 local MAX_DEPTH = 100
 
@@ -70,8 +72,10 @@ local function move(where, what, skip_gitignored)
   end
 end
 
+---@param node Node
 local function expand_node(node)
-  if not node.open then
+  if node:is(DirectoryNode) and not node.open then
+    ---@cast node DirectoryNode
     -- Expand the node.
     -- Should never collapse since we checked open.
     node:expand_or_collapse()
@@ -96,7 +100,8 @@ local function move_next_recursive(what, skip_gitignored)
   if node_init.name ~= ".." then -- root node cannot have a status
     valid = status_is_valid(node_init, what, skip_gitignored)
   end
-  if node_init.nodes ~= nil and valid and not node_init.open then
+  if node_init:is(DirectoryNode) and valid and not node_init.open then
+    ---@cast node_init DirectoryNode
     node_init:expand_or_collapse()
   end
 
diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua
index c153c07ad60..c6b5de44b53 100644
--- a/lua/nvim-tree/api.lua
+++ b/lua/nvim-tree/api.lua
@@ -9,6 +9,8 @@ local help = require("nvim-tree.help")
 local keymap = require("nvim-tree.keymap")
 local notify = require("nvim-tree.notify")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local Api = {
   tree = {},
   node = {
@@ -213,7 +215,8 @@ local function open_or_expand_or_dir_up(mode, toggle_group)
   return function(node)
     if node.name == ".." then
       actions.root.change_dir.fn("..")
-    elseif node.nodes then
+    elseif node:is(DirectoryNode) then
+      ---@cast node DirectoryNode
       node:expand_or_collapse(toggle_group)
     elseif not toggle_group then
       edit(mode, node)
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 4bb216b7518..439034afd3e 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -116,6 +116,35 @@ function DirectoryNode:get_git_status()
   end
 end
 
+function DirectoryNode:expand_or_collapse(toggle_group)
+  toggle_group = toggle_group or false
+  if self.has_children then
+    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 DirectoryNode cloned
 function DirectoryNode:clone()
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index 086177fcf85..6c51c19c993 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -225,36 +225,6 @@ function BaseNode:ungroup_empty_folders()
   end
 end
 
-function BaseNode:expand_or_collapse(toggle_group)
-  toggle_group = toggle_group or false
-  if self.has_children then
-    ---@cast self DirectoryNode -- TODO #2886 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()

From 8331a24c77ae52e217dcf05ac4989c189d84d6e5 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 20 Oct 2024 17:23:22 +1100
Subject: [PATCH 05/88] refactor(#2886): multi instance: node group functions
 refactoring (#2959)

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style
---
 lua/nvim-tree.lua                             |   2 +-
 lua/nvim-tree/actions/finders/find-file.lua   |   1 +
 lua/nvim-tree/actions/fs/clipboard.lua        |   4 +-
 lua/nvim-tree/actions/fs/create-file.lua      |  33 ++--
 lua/nvim-tree/actions/fs/rename-file.lua      |   6 +-
 lua/nvim-tree/actions/moves/item.lua          |   4 +-
 lua/nvim-tree/actions/moves/parent.lua        |  26 +--
 .../actions/tree/modifiers/collapse-all.lua   |   7 +-
 .../actions/tree/modifiers/expand-all.lua     |  16 +-
 lua/nvim-tree/api.lua                         |   9 +-
 lua/nvim-tree/class.lua                       |  40 +++++
 lua/nvim-tree/explorer/init.lua               |  37 ++--
 lua/nvim-tree/explorer/live-filter.lua        |  18 +-
 lua/nvim-tree/explorer/watch.lua              |   2 +-
 lua/nvim-tree/node/directory-link.lua         |   2 +-
 lua/nvim-tree/node/directory.lua              |  99 ++++++++--
 lua/nvim-tree/node/factory.lua                |   2 +-
 lua/nvim-tree/node/file-link.lua              |   2 +-
 lua/nvim-tree/node/file.lua                   |  11 +-
 lua/nvim-tree/node/init.lua                   | 170 ++++--------------
 lua/nvim-tree/renderer/builder.lua            |  33 ++--
 lua/nvim-tree/renderer/decorator/init.lua     |  18 +-
 22 files changed, 284 insertions(+), 258 deletions(-)
 create mode 100644 lua/nvim-tree/class.lua

diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua
index 16f2fa33ac3..7f9b23520fb 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 = node:get_parent_of_group() or node
 
   local line = vim.api.nvim_get_current_line()
   local cursor = vim.api.nvim_win_get_cursor(0)
diff --git a/lua/nvim-tree/actions/finders/find-file.lua b/lua/nvim-tree/actions/finders/find-file.lua
index 174ffbdbd81..fa5eea51369 100644
--- a/lua/nvim-tree/actions/finders/find-file.lua
+++ b/lua/nvim-tree/actions/finders/find-file.lua
@@ -43,6 +43,7 @@ function M.fn(path)
       return node.absolute_path == path_real or node.link_to == path_real
     end)
     :applier(function(node)
+      ---@cast node DirectoryNode
       local incremented_line = false
       if not node.group_next then
         line = line + 1
diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua
index cc75be4d13f..93c16e87606 100644
--- a/lua/nvim-tree/actions/fs/clipboard.lua
+++ b/lua/nvim-tree/actions/fs/clipboard.lua
@@ -7,6 +7,8 @@ local notify = require("nvim-tree.notify")
 
 local find_file = require("nvim-tree.actions.finders.find-file").fn
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 ---@enum ACTION
 local ACTION = {
   copy = "copy",
@@ -219,7 +221,7 @@ end
 function Clipboard:do_paste(node, action, action_fn)
   if node.name == ".." then
     node = self.explorer
-  else
+  elseif node:is(DirectoryNode) then
     node = node:last_group_node()
   end
   local clip = self.data[action]
diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua
index 588c978fc92..455f6f564f7 100644
--- a/lua/nvim-tree/actions/fs/create-file.lua
+++ b/lua/nvim-tree/actions/fs/create-file.lua
@@ -5,6 +5,9 @@ local notify = require("nvim-tree.notify")
 
 local find_file = require("nvim-tree.actions.finders.find-file").fn
 
+local FileNode = require("nvim-tree.node.file")
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 
 ---@param file string
@@ -29,35 +32,21 @@ local function get_num_nodes(iter)
   return i
 end
 
----@param node Node
----@return string
-local function get_containing_folder(node)
-  if node.nodes ~= nil then
-    return utils.path_add_trailing(node.absolute_path)
-  end
-  local node_name_size = #(node.name or "")
-  return node.absolute_path:sub(0, -node_name_size - 1)
-end
-
 ---@param node Node?
 function M.fn(node)
-  local cwd = core.get_cwd()
-  if cwd == nil then
+  node = node or core.get_explorer() --[[@as Node]]
+  if not node then
     return
   end
 
-  if not node or node.name == ".." then
-    node = {
-      absolute_path = cwd,
-      name = "",
-      nodes = core.get_explorer().nodes,
-      open = true,
-    }
-  else
-    node = node:last_group_node()
+  local dir = node:is(FileNode) and node.parent or node:as(DirectoryNode)
+  if not dir then
+    return
   end
 
-  local containing_folder = get_containing_folder(node)
+  dir = dir:last_group_node()
+
+  local containing_folder = utils.path_add_trailing(dir.absolute_path)
 
   local input_opts = {
     prompt = "Create file ",
diff --git a/lua/nvim-tree/actions/fs/rename-file.lua b/lua/nvim-tree/actions/fs/rename-file.lua
index a06fb201c56..d60cbb4969d 100644
--- a/lua/nvim-tree/actions/fs/rename-file.lua
+++ b/lua/nvim-tree/actions/fs/rename-file.lua
@@ -6,6 +6,8 @@ local notify = require("nvim-tree.notify")
 
 local find_file = require("nvim-tree.actions.finders.find-file").fn
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {
   config = {},
 }
@@ -120,7 +122,9 @@ function M.fn(default_modifier)
       return
     end
 
-    node = node:last_group_node()
+    if node:is(DirectoryNode) then
+      node = node:last_group_node()
+    end
     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 a327f0b0bef..2b081273626 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -78,7 +78,7 @@ local function expand_node(node)
     ---@cast node DirectoryNode
     -- Expand the node.
     -- Should never collapse since we checked open.
-    node:expand_or_collapse()
+    node:expand_or_collapse(false)
   end
 end
 
@@ -102,7 +102,7 @@ local function move_next_recursive(what, skip_gitignored)
   end
   if node_init:is(DirectoryNode) and valid and not node_init.open then
     ---@cast node_init DirectoryNode
-    node_init:expand_or_collapse()
+    node_init:expand_or_collapse(false)
   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 88eca47567a..9502af46f1c 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 DirectoryNode = require("nvim-tree.node.directory")
 
 local M = {}
 
@@ -9,33 +10,32 @@ local M = {}
 function M.fn(should_close)
   should_close = should_close or false
 
+  ---@param node Node
   return function(node)
-    local explorer = core.get_explorer()
-    node = node:last_group_node()
-    if should_close and node.open then
-      node.open = false
-      if explorer then
-        explorer.renderer:draw()
+    local dir = node:as(DirectoryNode)
+    if dir then
+      dir = dir:last_group_node()
+      if should_close and dir.open then
+        dir.open = false
+        dir.explorer.renderer:draw()
+        return
       end
-      return
     end
 
-    local parent = node:get_parent_of_group().parent
+    local parent = (node:get_parent_of_group() or node).parent
 
     if not parent or not parent.parent then
       return view.set_cursor({ 1, 0 })
     end
 
-    local _, line = utils.find_node(core.get_explorer().nodes, function(n)
+    local _, line = utils.find_node(parent.explorer.nodes, function(n)
       return n.absolute_path == parent.absolute_path
     end)
 
     view.set_cursor({ line + 1, 0 })
     if should_close then
       parent.open = false
-      if explorer then
-        explorer.renderer:draw()
-      end
+      parent.explorer.renderer:draw()
     end
   end
 end
diff --git a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
index 136bd357297..214d572c035 100644
--- a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
@@ -3,6 +3,8 @@ local core = require("nvim-tree.core")
 local lib = require("nvim-tree.lib")
 local Iterator = require("nvim-tree.iterators.node-iterator")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 
 ---@return fun(path: string): boolean
@@ -36,8 +38,9 @@ function M.fn(keep_buffers)
   Iterator.builder(explorer.nodes)
     :hidden()
     :applier(function(n)
-      if n.nodes ~= nil then
-        n.open = keep_buffers == true and matches(n.absolute_path)
+      local dir = n:as(DirectoryNode)
+      if dir then
+        dir.open = keep_buffers and matches(dir.absolute_path)
       end
     end)
     :recursor(function(n)
diff --git a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
index ed898de2a30..ffede0e6d6f 100644
--- a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
@@ -2,6 +2,8 @@ local core = require("nvim-tree.core")
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local notify = require("nvim-tree.notify")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 
 ---@param list string[]
@@ -15,7 +17,7 @@ local function to_lookup_table(list)
   return table
 end
 
----@param node Node
+---@param node DirectoryNode
 local function expand(node)
   node = node:last_group_node()
   node.open = true
@@ -36,6 +38,7 @@ end
 local function gen_iterator()
   local expansion_count = 0
 
+  ---@param parent DirectoryNode
   return function(parent)
     if parent.parent and parent.nodes and not parent.open then
       expansion_count = expansion_count + 1
@@ -44,12 +47,14 @@ local function gen_iterator()
 
     Iterator.builder(parent.nodes)
       :hidden()
+    ---@param node DirectoryNode
       :applier(function(node)
         if should_expand(expansion_count, node) then
           expansion_count = expansion_count + 1
           expand(node)
         end
       end)
+    ---@param node DirectoryNode
       :recursor(function(node)
         return expansion_count < M.MAX_FOLDER_DISCOVERY and (node.group_next and { node.group_next } or (node.open and node.nodes))
       end)
@@ -61,11 +66,16 @@ local function gen_iterator()
   end
 end
 
+---Expand the directory node or the root
 ---@param node Node
 function M.fn(node)
   local explorer = core.get_explorer()
-  node = node.nodes and node or explorer
-  if gen_iterator()(node) then
+  local parent = node:as(DirectoryNode) or explorer
+  if not parent then
+    return
+  end
+
+  if gen_iterator()(parent) then
     notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders")
   end
   if explorer then
diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua
index c6b5de44b53..7daab9a1ae2 100644
--- a/lua/nvim-tree/api.lua
+++ b/lua/nvim-tree/api.lua
@@ -10,6 +10,7 @@ local keymap = require("nvim-tree.keymap")
 local notify = require("nvim-tree.notify")
 
 local DirectoryNode = require("nvim-tree.node.directory")
+local RootNode = require("nvim-tree.node.root")
 
 local Api = {
   tree = {},
@@ -137,9 +138,9 @@ Api.tree.change_root = wrap(function(...)
 end)
 
 Api.tree.change_root_to_node = wrap_node(function(node)
-  if node.name == ".." then
+  if node.name == ".." or node:is(RootNode) then
     actions.root.change_dir.fn("..")
-  elseif node.nodes ~= nil then
+  elseif node:is(DirectoryNode) then
     actions.root.change_dir.fn(node:last_group_node().absolute_path)
   end
 end)
@@ -210,13 +211,13 @@ local function edit(mode, node)
 end
 
 ---@param mode string
----@return fun(node: table)
+---@return fun(node: Node)
 local function open_or_expand_or_dir_up(mode, toggle_group)
+  ---@param node Node
   return function(node)
     if node.name == ".." then
       actions.root.change_dir.fn("..")
     elseif node:is(DirectoryNode) then
-      ---@cast node DirectoryNode
       node:expand_or_collapse(toggle_group)
     elseif not toggle_group then
       edit(mode, node)
diff --git a/lua/nvim-tree/class.lua b/lua/nvim-tree/class.lua
new file mode 100644
index 00000000000..8565e3c6ded
--- /dev/null
+++ b/lua/nvim-tree/class.lua
@@ -0,0 +1,40 @@
+---Generic class, useful for inheritence.
+---@class (exact) Class
+---@field private __index? table
+local Class = {}
+
+---@param o Class?
+---@return Class
+function Class:new(o)
+  o = o or {}
+
+  setmetatable(o, self)
+  self.__index = self
+
+  return o
+end
+
+---Object is an instance of class
+---This will start with the lowest class and loop over all the superclasses.
+---@param class table
+---@return boolean
+function Class:is(class)
+  local mt = getmetatable(self)
+  while mt do
+    if mt == class then
+      return true
+    end
+    mt = getmetatable(mt)
+  end
+  return false
+end
+
+---Return object if it is an instance of class, otherwise nil
+---@generic T
+---@param class T
+---@return `T`|nil
+function Class:as(class)
+  return self:is(class) and self or nil
+end
+
+return Class
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 0045ba9b243..b0cbc99665a 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -5,6 +5,7 @@ local utils = require("nvim-tree.utils")
 local view = require("nvim-tree.view")
 local node_factory = require("nvim-tree.node.factory")
 
+local DirectoryNode = require("nvim-tree.node.directory")
 local RootNode = require("nvim-tree.node.root")
 local Watcher = require("nvim-tree.watcher")
 
@@ -72,12 +73,12 @@ function Explorer:create(path)
   return o
 end
 
----@param node Node
+---@param node DirectoryNode
 function Explorer:expand(node)
   self:_load(node)
 end
 
----@param node Node
+---@param node DirectoryNode
 ---@param git_status table|nil
 function Explorer:reload(node, git_status)
   local cwd = node.link_to or node.absolute_path
@@ -169,11 +170,10 @@ function Explorer:reload(node, git_status)
     end, node.nodes)
   )
 
-  local is_root = not node.parent
-  local child_folder_only = node:has_one_child_folder() 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)
+  local single_child = node:single_child_directory()
+  if config.renderer.group_empty and node.parent and single_child then
+    node.group_next = single_child
+    local ns = self:reload(single_child, git_status)
     node.nodes = ns or {}
     log.profile_end(profile)
     return ns
@@ -219,7 +219,7 @@ function Explorer:refresh_parent_nodes_for_path(path)
 end
 
 ---@private
----@param node Node
+---@param node DirectoryNode
 function Explorer:_load(node)
   local cwd = node.link_to or node.absolute_path
   local git_status = git.load_project_status(cwd)
@@ -243,7 +243,7 @@ end
 ---@private
 ---@param handle uv.uv_fs_t
 ---@param cwd string
----@param node Node
+---@param node DirectoryNode
 ---@param git_status table
 ---@param parent Explorer
 function Explorer:populate_children(handle, cwd, node, git_status, parent)
@@ -295,7 +295,7 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent)
 end
 
 ---@private
----@param node Node
+---@param node DirectoryNode
 ---@param status table
 ---@param parent Explorer
 ---@return Node[]|nil
@@ -311,12 +311,12 @@ 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]
-  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 single_child = node:single_child_directory()
+  if config.renderer.group_empty and not is_root and single_child then
+    local child_cwd = single_child.link_to or single_child.absolute_path
     local child_status = git.load_project_status(child_cwd)
-    node.group_next = child_folder_only
-    local ns = self:explore(child_folder_only, child_status, parent)
+    node.group_next = single_child
+    local ns = self:explore(single_child, child_status, parent)
     node.nodes = ns or {}
 
     log.profile_end(profile)
@@ -335,9 +335,10 @@ end
 function Explorer:refresh_nodes(projects)
   Iterator.builder({ self })
     :applier(function(n)
-      if n.nodes then
-        local toplevel = git.get_toplevel(n.cwd or n.link_to or n.absolute_path)
-        self:reload(n, projects[toplevel] or {})
+      local dir = n:as(DirectoryNode)
+      if dir then
+        local toplevel = git.get_toplevel(dir.cwd or dir.link_to or dir.absolute_path)
+        self:reload(dir, projects[toplevel] or {})
       end
     end)
     :recursor(function(n)
diff --git a/lua/nvim-tree/explorer/live-filter.lua b/lua/nvim-tree/explorer/live-filter.lua
index 02cfda1c251..30152d63ae4 100644
--- a/lua/nvim-tree/explorer/live-filter.lua
+++ b/lua/nvim-tree/explorer/live-filter.lua
@@ -1,6 +1,8 @@
 local view = require("nvim-tree.view")
 local utils = require("nvim-tree.utils")
+
 local Iterator = require("nvim-tree.iterators.node-iterator")
+local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class LiveFilter
 ---@field explorer Explorer
@@ -31,17 +33,19 @@ local function reset_filter(self, node_)
     return
   end
 
-  node_.hidden_stats = vim.tbl_deep_extend("force", node_.hidden_stats or {}, {
-    live_filter = 0,
-  })
+  local dir_ = node_:as(DirectoryNode)
+  if dir_ then
+    dir_.hidden_stats = vim.tbl_deep_extend("force", dir_.hidden_stats or {}, { live_filter = 0, })
+  end
 
   Iterator.builder(node_.nodes)
     :hidden()
     :applier(function(node)
       node.hidden = false
-      node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
-        live_filter = 0,
-      })
+      local dir = node:as(DirectoryNode)
+      if dir then
+        dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
+      end
     end)
     :iterate()
 end
@@ -85,7 +89,7 @@ local function matches(self, node)
   return vim.regex(self.filter):match_str(name) ~= nil
 end
 
----@param node_ Node?
+---@param node_ DirectoryNode?
 function LiveFilter:apply_filter(node_)
   if not self.filter or self.filter == "" then
     reset_filter(self, node_)
diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua
index 7fd13f4c3fb..a83758132ca 100644
--- a/lua/nvim-tree/explorer/watch.lua
+++ b/lua/nvim-tree/explorer/watch.lua
@@ -53,7 +53,7 @@ local function is_folder_ignored(path)
   return false
 end
 
----@param node Node
+---@param node DirectoryNode
 ---@return Watcher|nil
 function M.create_watcher(node)
   if not M.config.filesystem_watchers.enable or type(node) ~= "table" then
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index c8759541e22..f0543825fe9 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -9,7 +9,7 @@ local DirectoryLinkNode = DirectoryNode:new()
 
 ---Static factory method
 ---@param explorer Explorer
----@param parent Node
+---@param parent DirectoryNode
 ---@param absolute_path string
 ---@param link_to string
 ---@param name string
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 439034afd3e..cf423364a74 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,19 +1,20 @@
 local git = require("nvim-tree.git")
 local watch = require("nvim-tree.explorer.watch")
 
-local BaseNode = require("nvim-tree.node")
+local Node = require("nvim-tree.node")
 
----@class (exact) DirectoryNode: BaseNode
+---@class (exact) DirectoryNode: Node
 ---@field has_children boolean
----@field group_next Node? -- If node is grouped, this points to the next child dir/link node
+---@field group_next DirectoryNode? -- If node is grouped, this points to the next child dir/link node
 ---@field nodes Node[]
 ---@field open boolean
+---@field watcher Watcher?
 ---@field hidden_stats table? -- Each field of this table is a key for source and value for count
-local DirectoryNode = BaseNode:new()
+local DirectoryNode = Node:new()
 
 ---Static factory method
 ---@param explorer Explorer
----@param parent Node?
+---@param parent DirectoryNode?
 ---@param absolute_path string
 ---@param name string
 ---@param fs_stat uv.fs_stat.result|nil
@@ -51,12 +52,18 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat)
 end
 
 function DirectoryNode:destroy()
-  BaseNode.destroy(self)
+  if self.watcher then
+    self.watcher:destroy()
+    self.watcher = nil
+  end
+
   if self.nodes then
     for _, node in pairs(self.nodes) do
       node:destroy()
     end
   end
+
+  Node.destroy(self)
 end
 
 ---Update the GitStatus of the directory
@@ -116,6 +123,75 @@ function DirectoryNode:get_git_status()
   end
 end
 
+---Refresh contents and git status for a single node
+function DirectoryNode:refresh()
+  local node = self:get_parent_of_group() or self
+  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(node, project)
+
+    node:update_parent_statuses(project, toplevel)
+
+    self.explorer.renderer:draw()
+  end)
+end
+
+-- If node is grouped, return the last node in the group. Otherwise, return the given node.
+---@return DirectoryNode
+function DirectoryNode:last_group_node()
+  return self.group_next and self.group_next:last_group_node() or self
+end
+
+---Return the one and only one child directory
+---@return DirectoryNode?
+function DirectoryNode:single_child_directory()
+  if #self.nodes == 1 then
+    return self.nodes[1]:as(DirectoryNode)
+  end
+end
+
+---@private
+-- Toggle group empty folders
+function DirectoryNode: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
+---@private
+---@return Node[]
+function DirectoryNode:group_empty_folders()
+  local single_child = self:single_child_directory()
+  if self.explorer.opts.renderer.group_empty and self.parent and single_child then
+    self.group_next = single_child
+    local ns = single_child: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
+---@private
+function DirectoryNode:ungroup_empty_folders()
+  if self.group_next then
+    self.group_next:ungroup_empty_folders()
+    self.nodes = { self.group_next }
+    self.group_next = nil
+  end
+end
+
+---@param toggle_group boolean
 function DirectoryNode:expand_or_collapse(toggle_group)
   toggle_group = toggle_group or false
   if self.has_children then
@@ -126,7 +202,7 @@ function DirectoryNode:expand_or_collapse(toggle_group)
     self.explorer:expand(self)
   end
 
-  local head_node = self:get_parent_of_group()
+  local head_node = self:get_parent_of_group() or self
   if toggle_group then
     head_node:toggle_group_folders()
   end
@@ -138,8 +214,11 @@ function DirectoryNode:expand_or_collapse(toggle_group)
   else
     next_open = not open
   end
-  for _, n in ipairs(head_node:get_all_nodes_in_group()) do
-    n.open = next_open
+
+  local node = self
+  while node do
+    node.open = next_open
+    node = node.group_next
   end
 
   self.explorer.renderer:draw()
@@ -148,7 +227,7 @@ 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]]
+  local clone = Node.clone(self) --[[@as DirectoryNode]]
 
   clone.has_children = self.has_children
   clone.group_next = nil
diff --git a/lua/nvim-tree/node/factory.lua b/lua/nvim-tree/node/factory.lua
index e6a7e7b437a..ee0504fc807 100644
--- a/lua/nvim-tree/node/factory.lua
+++ b/lua/nvim-tree/node/factory.lua
@@ -8,7 +8,7 @@ local M = {}
 
 ---Factory function to create the appropriate Node
 ---@param explorer Explorer
----@param parent Node
+---@param parent DirectoryNode
 ---@param absolute_path string
 ---@param stat uv.fs_stat.result? -- on nil stat return nil Node
 ---@param name string
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 60c50d771f9..2bdd79f13f3 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -9,7 +9,7 @@ local FileLinkNode = FileNode:new()
 
 ---Static factory method
 ---@param explorer Explorer
----@param parent Node
+---@param parent DirectoryNode
 ---@param absolute_path string
 ---@param link_to string
 ---@param name string
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index cbf3f96df5a..0f01347c0cf 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -1,15 +1,15 @@
 local git = require("nvim-tree.git")
 local utils = require("nvim-tree.utils")
 
-local BaseNode = require("nvim-tree.node")
+local Node = require("nvim-tree.node")
 
----@class (exact) FileNode: BaseNode
+---@class (exact) FileNode: Node
 ---@field extension string
-local FileNode = BaseNode:new()
+local FileNode = Node:new()
 
 ---Static factory method
 ---@param explorer Explorer
----@param parent Node
+---@param parent DirectoryNode
 ---@param absolute_path string
 ---@param name string
 ---@param fs_stat uv.fs_stat.result?
@@ -27,7 +27,6 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat)
     is_dot = false,
     name = name,
     parent = parent,
-    watcher = nil,
     diag_status = nil,
 
     extension = string.match(name, ".?[^.]+%.(.*)") or "",
@@ -56,7 +55,7 @@ end
 ---Create a sanitized partial copy of a node
 ---@return FileNode cloned
 function FileNode:clone()
-  local clone = BaseNode.clone(self) --[[@as FileNode]]
+  local clone = Node.clone(self) --[[@as FileNode]]
 
   clone.extension = self.extension
 
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index ceca46ce308..a2af3afe529 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -1,12 +1,14 @@
 local git = require("nvim-tree.git")
 
+local Class = require("nvim-tree.class")
+
+---TODO #2886
 ---TODO remove all @cast
 ---TODO remove all references to directory fields:
 
 ---Abstract Node class.
 ---Uses the abstract factory pattern to instantiate child instances.
----@class (exact) BaseNode
----@field private __index? table
+---@class (exact) Node: Class
 ---@field type NODE_TYPE
 ---@field explorer Explorer
 ---@field absolute_path string
@@ -15,68 +17,30 @@ local git = require("nvim-tree.git")
 ---@field git_status GitStatus?
 ---@field hidden boolean
 ---@field name string
----@field parent Node?
----@field watcher Watcher?
+---@field parent DirectoryNode?
 ---@field diag_status DiagStatus?
 ---@field is_dot boolean cached is_dotfile
-local BaseNode = {}
-
----@alias Node RootNode|BaseNode|DirectoryNode|FileNode|DirectoryLinkNode|FileLinkNode
-
----@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
+local Node = Class:new()
 
----@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
+function Node:destroy()
 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
+function Node:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local
+  ---TODO find a way to declare abstract methods
 end
+
 --luacheck: pop
 
 ---@return GitStatus?
-function BaseNode:get_git_status()
+function Node:get_git_status()
 end
 
 ---@param projects table
-function BaseNode:reload_node_status(projects)
+function Node:reload_node_status(projects)
   local toplevel = git.get_toplevel(self.absolute_path)
   local status = projects[toplevel] or {}
   for _, node in ipairs(self.nodes) do
@@ -88,13 +52,13 @@ function BaseNode:reload_node_status(projects)
 end
 
 ---@return boolean
-function BaseNode:is_git_ignored()
+function Node:is_git_ignored()
   return self.git_status ~= nil and self.git_status.file == "!!"
 end
 
 ---Node or one of its parents begins with a dot
 ---@return boolean
-function BaseNode:is_dotfile()
+function Node:is_dotfile()
   if
     self.is_dot
     or (self.name and (self.name:sub(1, 1) == "."))
@@ -106,22 +70,9 @@ 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
-  --- @cast node 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)
+function Node:update_parent_statuses(project, root)
   local node = self
   while project and node do
     -- step up to the containing project
@@ -151,88 +102,30 @@ function BaseNode:update_parent_statuses(project, root)
   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
+---Get the highest parent of grouped nodes, nil when not grouped
+---@return DirectoryNode?
+function Node:get_parent_of_group()
+  if not self.parent or not self.parent.group_next then
+    return nil
   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 #2886 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]
+  local node = self.parent
+  while node do
+    if node.parent and node.parent.group_next then
+      node = node.parent
+    else
+      return node
+    end
   end
 end
 
 ---Create a sanitized partial copy of a node, populating children recursively.
----@return BaseNode cloned
-function BaseNode:clone()
+---@return Node cloned
+function Node:clone()
   ---@type Explorer
   local explorer_placeholder = nil
 
-  ---@type BaseNode
+  ---@type Node
   local clone = {
     type = self.type,
     explorer = explorer_placeholder,
@@ -244,11 +137,10 @@ function BaseNode:clone()
     is_dot = self.is_dot,
     name = self.name,
     parent = nil,
-    watcher = nil,
     diag_status = nil,
   }
 
   return clone
 end
 
-return BaseNode
+return Node
diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua
index 2071bdabcac..9804bfcc4a0 100644
--- a/lua/nvim-tree/renderer/builder.lua
+++ b/lua/nvim-tree/renderer/builder.lua
@@ -2,6 +2,10 @@ local notify = require("nvim-tree.notify")
 local utils = require("nvim-tree.utils")
 local view = require("nvim-tree.view")
 
+local DirectoryLinkNode = require("nvim-tree.node.directory-link")
+local DirectoryNode = require("nvim-tree.node.directory")
+local FileLinkNode = require("nvim-tree.node.file-link")
+
 local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks")
 local DecoratorCopied = require("nvim-tree.renderer.decorator.copied")
 local DecoratorCut = require("nvim-tree.renderer.decorator.cut")
@@ -341,19 +345,21 @@ function Builder:add_highlights(node)
   return icon_hl_group, name_hl_group
 end
 
+---Insert node line into self.lines, calling Builder:build_lines for each directory
 ---@private
+---@param node Node
+---@param idx integer line number starting at 1
+---@param num_children integer of node
 function Builder:build_line(node, idx, num_children)
   -- various components
   local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers)
   local arrows = pad.get_arrows(node)
 
   -- main components
-  local is_folder = node.nodes ~= nil
-  local is_symlink = node.link_to ~= nil
   local icon, name
-  if is_folder then
+  if node:is(DirectoryNode) then
     icon, name = self:build_folder(node)
-  elseif is_symlink then
+  elseif node:is(DirectoryLinkNode) or node:is(FileLinkNode) then
     icon, name = self:build_symlink(node)
   else
     icon, name = self:build_file(node)
@@ -369,11 +375,13 @@ function Builder:build_line(node, idx, num_children)
 
   self.index = self.index + 1
 
-  node = node:last_group_node()
-  if node.open then
-    self.depth = self.depth + 1
-    self:build_lines(node)
-    self.depth = self.depth - 1
+  if node:is(DirectoryNode) then
+    node = node:last_group_node()
+    if node.open then
+      self.depth = self.depth + 1
+      self:build_lines(node)
+      self.depth = self.depth - 1
+    end
   end
 end
 
@@ -403,8 +411,11 @@ function Builder:add_hidden_count_string(node, idx, num_children)
   end
 end
 
+---Number of visible nodes
 ---@private
-function Builder:get_nodes_number(nodes)
+---@param nodes Node[]
+---@return integer
+function Builder:num_visible(nodes)
   if not self.explorer.live_filter.filter then
     return #nodes
   end
@@ -423,7 +434,7 @@ function Builder:build_lines(node)
   if not node then
     node = self.explorer
   end
-  local num_children = self:get_nodes_number(node.nodes)
+  local num_children = self:num_visible(node.nodes)
   local idx = 1
   for _, n in ipairs(node.nodes) do
     if not n.hidden then
diff --git a/lua/nvim-tree/renderer/decorator/init.lua b/lua/nvim-tree/renderer/decorator/init.lua
index a80ce615a05..44f05f8e23e 100644
--- a/lua/nvim-tree/renderer/decorator/init.lua
+++ b/lua/nvim-tree/renderer/decorator/init.lua
@@ -1,26 +1,16 @@
+local Class = require("nvim-tree.class")
+
 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
+---@class (exact) Decorator: Class
 ---@field protected explorer Explorer
 ---@field protected enabled boolean
 ---@field protected hl_pos HL_POSITION
 ---@field protected icon_placement ICON_PLACEMENT
-local Decorator = {}
-
----@param o Decorator|nil
----@return Decorator
-function Decorator:new(o)
-  o = o or {}
-
-  setmetatable(o, self)
-  self.__index = self
-
-  return o
-end
+local Decorator = Class:new()
 
 ---Maybe highlight groups
 ---@param node Node

From 0992969dc50b486742e085c02621aa3efc238e2a Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 21 Oct 2024 10:56:45 +1100
Subject: [PATCH 06/88] move lib.get_cursor_position to Explorer

---
 lua/nvim-tree/actions/moves/item.lua |  9 +++++++--
 lua/nvim-tree/explorer/init.lua      | 13 +++++++++++++
 lua/nvim-tree/lib.lua                | 18 +-----------------
 3 files changed, 21 insertions(+), 19 deletions(-)

diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index 2b081273626..462e2870931 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -34,11 +34,16 @@ end
 ---@param what string type of status
 ---@param skip_gitignored boolean default false
 local function move(where, what, skip_gitignored)
+  local explorer = core.get_explorer()
+  if not explorer then
+    return
+  end
+
   local first_node_line = core.get_nodes_starting_line()
-  local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, first_node_line)
+  local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line)
   local iter_start, iter_end, iter_step, cur, first, nex
 
-  local cursor = lib.get_cursor_position()
+  local cursor = explorer:get_cursor_position()
   if cursor and cursor[1] < first_node_line then
     cur = cursor[1]
   end
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index b0cbc99665a..6dbbd1720c9 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -374,6 +374,19 @@ function Explorer:reload_git()
   event_running = false
 end
 
+---Cursor position as per vim.api.nvim_win_get_cursor
+---nil on no explorer or invalid view win
+---@return integer[]|nil
+function Explorer:get_cursor_position()
+  local winnr = view.get_winnr()
+  if not winnr or not vim.api.nvim_win_is_valid(winnr) then
+    return
+  end
+
+  return vim.api.nvim_win_get_cursor(winnr)
+end
+
+
 function Explorer:setup(opts)
   config = opts
   require("nvim-tree.explorer.watch").setup(opts)
diff --git a/lua/nvim-tree/lib.lua b/lua/nvim-tree/lib.lua
index 15ccb337aa1..2b9ead582aa 100644
--- a/lua/nvim-tree/lib.lua
+++ b/lua/nvim-tree/lib.lua
@@ -13,22 +13,6 @@ local M = {
   target_winid = nil,
 }
 
----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
-    return
-  end
-
-  local winnr = view.get_winnr()
-  if not winnr or not vim.api.nvim_win_is_valid(winnr) then
-    return
-  end
-
-  return vim.api.nvim_win_get_cursor(winnr)
-end
-
 ---@return Node|nil
 function M.get_node_at_cursor()
   local explorer = core.get_explorer()
@@ -36,7 +20,7 @@ function M.get_node_at_cursor()
     return
   end
 
-  local cursor = M.get_cursor_position()
+  local cursor = explorer:get_cursor_position()
   if not cursor then
     return
   end

From 7324fb1bf0df95d8fad879e432c70cf80ec34895 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 21 Oct 2024 11:34:10 +1100
Subject: [PATCH 07/88] move lib.get_node_at_cursor to Explorer

---
 lua/nvim-tree.lua                             |  8 +++-
 lua/nvim-tree/actions/fs/rename-file.lua      | 16 +++----
 lua/nvim-tree/actions/moves/item.lua          | 48 ++++++++++---------
 .../actions/tree/modifiers/collapse-all.lua   |  8 ++--
 .../actions/tree/modifiers/toggles.lua        |  7 +--
 lua/nvim-tree/api.lua                         | 32 ++++++-------
 lua/nvim-tree/explorer/init.lua               | 14 ++++++
 lua/nvim-tree/explorer/live-filter.lua        |  4 +-
 lua/nvim-tree/lib.lua                         | 20 --------
 lua/nvim-tree/log.lua                         |  4 +-
 lua/nvim-tree/marks/init.lua                  |  4 +-
 11 files changed, 83 insertions(+), 82 deletions(-)

diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua
index 7f9b23520fb..a45c5c6c820 100644
--- a/lua/nvim-tree.lua
+++ b/lua/nvim-tree.lua
@@ -1,4 +1,3 @@
-local lib = require("nvim-tree.lib")
 local log = require("nvim-tree.log")
 local appearance = require("nvim-tree.appearance")
 local view = require("nvim-tree.view")
@@ -121,7 +120,12 @@ function M.place_cursor_on_node()
     return
   end
 
-  local node = lib.get_node_at_cursor()
+  local explorer = core.get_explorer()
+  if not explorer then
+    return
+  end
+
+  local node = explorer:get_node_at_cursor()
   if not node or node.name == ".." then
     return
   end
diff --git a/lua/nvim-tree/actions/fs/rename-file.lua b/lua/nvim-tree/actions/fs/rename-file.lua
index d60cbb4969d..da1ca001167 100644
--- a/lua/nvim-tree/actions/fs/rename-file.lua
+++ b/lua/nvim-tree/actions/fs/rename-file.lua
@@ -1,5 +1,4 @@
 local core = require("nvim-tree.core")
-local lib = require("nvim-tree.lib")
 local utils = require("nvim-tree.utils")
 local events = require("nvim-tree.events")
 local notify = require("nvim-tree.notify")
@@ -104,11 +103,15 @@ function M.fn(default_modifier)
   default_modifier = default_modifier or ":t"
 
   return function(node, modifier)
-    if type(node) ~= "table" then
-      node = lib.get_node_at_cursor()
+    local explorer = core.get_explorer()
+    if not explorer then
+      return
     end
 
-    if node == nil then
+    if type(node) ~= "table" then
+      node = explorer:get_node_at_cursor()
+    end
+    if not node then
       return
     end
 
@@ -160,10 +163,7 @@ function M.fn(default_modifier)
 
       M.rename(node, prepend .. new_file_path .. append)
       if not M.config.filesystem_watchers.enable then
-        local explorer = core.get_explorer()
-        if explorer then
-          explorer:reload_explorer()
-        end
+        explorer:reload_explorer()
       end
 
       find_file(utils.path_remove_trailing(new_file_path))
diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index 462e2870931..9979963189a 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -1,7 +1,6 @@
 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 diagnostics = require("nvim-tree.diagnostics")
 
 local DirectoryNode = require("nvim-tree.node.directory")
@@ -30,15 +29,11 @@ local function status_is_valid(node, what, skip_gitignored)
 end
 
 ---Move to the next node that has a valid status. If none found, don't move.
+---@param explorer Explorer
 ---@param where string where to move (forwards or backwards)
 ---@param what string type of status
 ---@param skip_gitignored boolean default false
-local function move(where, what, skip_gitignored)
-  local explorer = core.get_explorer()
-  if not explorer then
-    return
-  end
-
+local function move(explorer, where, what, skip_gitignored)
   local first_node_line = core.get_nodes_starting_line()
   local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line)
   local iter_start, iter_end, iter_step, cur, first, nex
@@ -88,16 +83,17 @@ local function expand_node(node)
 end
 
 --- Move to the next node recursively.
+---@param explorer Explorer
 ---@param what string type of status
 ---@param skip_gitignored boolean default false
-local function move_next_recursive(what, skip_gitignored)
+local function move_next_recursive(explorer, what, skip_gitignored)
   -- If the current node:
   -- * is a directory
   -- * and is not the root node
   -- * and has a git/diag status
   -- * and is not opened
   -- expand it.
-  local node_init = lib.get_node_at_cursor()
+  local node_init = explorer:get_node_at_cursor()
   if not node_init then
     return
   end
@@ -110,9 +106,9 @@ local function move_next_recursive(what, skip_gitignored)
     node_init:expand_or_collapse(false)
   end
 
-  move("next", what, skip_gitignored)
+  move(explorer, "next", what, skip_gitignored)
 
-  local node_cur = lib.get_node_at_cursor()
+  local node_cur = explorer:get_node_at_cursor()
   if not node_cur then
     return
   end
@@ -128,10 +124,10 @@ local function move_next_recursive(what, skip_gitignored)
   while is_dir and i < MAX_DEPTH do
     expand_node(node_cur)
 
-    move("next", what, skip_gitignored)
+    move(explorer, "next", what, skip_gitignored)
 
     -- Save current node.
-    node_cur = lib.get_node_at_cursor()
+    node_cur = explorer:get_node_at_cursor()
     -- Update is_dir.
     if node_cur then
       is_dir = node_cur.nodes ~= nil
@@ -158,24 +154,25 @@ end
 --- 4.4) Call a non-recursive prev.
 --- 4.5) Save the current node and start back from 4.1.
 ---
+---@param explorer Explorer
 ---@param what string type of status
 ---@param skip_gitignored boolean default false
-local function move_prev_recursive(what, skip_gitignored)
+local function move_prev_recursive(explorer, what, skip_gitignored)
   local node_init, node_cur
 
   -- 1)
-  node_init = lib.get_node_at_cursor()
+  node_init = explorer:get_node_at_cursor()
   if node_init == nil then
     return
   end
 
   -- 2)
-  move("prev", what, skip_gitignored)
+  move(explorer, "prev", what, skip_gitignored)
 
-  node_cur = lib.get_node_at_cursor()
+  node_cur = explorer:get_node_at_cursor()
   if node_cur == node_init.parent then
     -- 3)
-    move_prev_recursive(what, skip_gitignored)
+    move_prev_recursive(explorer, what, skip_gitignored)
   else
     -- i is used to limit iterations.
     local i = 0
@@ -205,10 +202,10 @@ local function move_prev_recursive(what, skip_gitignored)
       end
 
       -- 4.4)
-      move("prev", what, skip_gitignored)
+      move(explorer, "prev", what, skip_gitignored)
 
       -- 4.5)
-      node_cur = lib.get_node_at_cursor()
+      node_cur = explorer:get_node_at_cursor()
 
       i = i + 1
     end
@@ -223,6 +220,11 @@ end
 ---@return fun()
 function M.fn(opts)
   return function()
+    local explorer = core.get_explorer()
+    if not explorer then
+      return
+    end
+
     local recurse = false
     local skip_gitignored = false
 
@@ -236,14 +238,14 @@ function M.fn(opts)
     end
 
     if not recurse then
-      move(opts.where, opts.what, skip_gitignored)
+      move(explorer, opts.where, opts.what, skip_gitignored)
       return
     end
 
     if opts.where == "next" then
-      move_next_recursive(opts.what, skip_gitignored)
+      move_next_recursive(explorer, opts.what, skip_gitignored)
     elseif opts.where == "prev" then
-      move_prev_recursive(opts.what, skip_gitignored)
+      move_prev_recursive(explorer, opts.what, skip_gitignored)
     end
   end
 end
diff --git a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
index 214d572c035..049be2bbfa2 100644
--- a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
@@ -1,6 +1,5 @@
 local utils = require("nvim-tree.utils")
 local core = require("nvim-tree.core")
-local lib = require("nvim-tree.lib")
 local Iterator = require("nvim-tree.iterators.node-iterator")
 
 local DirectoryNode = require("nvim-tree.node.directory")
@@ -26,10 +25,13 @@ end
 
 ---@param keep_buffers boolean
 function M.fn(keep_buffers)
-  local node = lib.get_node_at_cursor()
   local explorer = core.get_explorer()
+  if not explorer then
+    return
+  end
 
-  if explorer == nil then
+  local node = explorer:get_node_at_cursor()
+  if not node then
     return
   end
 
diff --git a/lua/nvim-tree/actions/tree/modifiers/toggles.lua b/lua/nvim-tree/actions/tree/modifiers/toggles.lua
index 782ab35e1dd..8a468a959f2 100644
--- a/lua/nvim-tree/actions/tree/modifiers/toggles.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/toggles.lua
@@ -1,13 +1,14 @@
-local lib = require("nvim-tree.lib")
 local utils = require("nvim-tree.utils")
 local core = require("nvim-tree.core")
 local M = {}
 
 ---@param explorer Explorer
 local function reload(explorer)
-  local node = lib.get_node_at_cursor()
+  local node = explorer:get_node_at_cursor()
   explorer:reload_explorer()
-  utils.focus_node_or_parent(node)
+  if node then
+    utils.focus_node_or_parent(node)
+  end
 end
 
 local function wrap_explorer(fn)
diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua
index 7daab9a1ae2..50fd9f2a4fc 100644
--- a/lua/nvim-tree/api.lua
+++ b/lua/nvim-tree/api.lua
@@ -55,11 +55,24 @@ local function wrap(f)
   end
 end
 
+---Invoke a method on the singleton explorer.
+---Print error when setup not called.
+---@param explorer_method string explorer method name
+---@return fun(...) : any
+local function wrap_explorer(explorer_method)
+  return wrap(function(...)
+    local explorer = core.get_explorer()
+    if explorer then
+      return explorer[explorer_method](explorer, ...)
+    end
+  end)
+end
+
 ---Inject the node as the first argument if present otherwise do nothing.
 ---@param fn function function to invoke
 local function wrap_node(fn)
   return function(node, ...)
-    node = node or lib.get_node_at_cursor()
+    node = node or wrap_explorer("get_node_at_cursor")()
     if node then
       return fn(node, ...)
     end
@@ -70,24 +83,11 @@ end
 ---@param fn function function to invoke
 local function wrap_node_or_nil(fn)
   return function(node, ...)
-    node = node or lib.get_node_at_cursor()
+    node = node or wrap_explorer("get_node_at_cursor")()
     return fn(node, ...)
   end
 end
 
----Invoke a method on the singleton explorer.
----Print error when setup not called.
----@param explorer_method string explorer method name
----@return fun(...) : any
-local function wrap_explorer(explorer_method)
-  return wrap(function(...)
-    local explorer = core.get_explorer()
-    if explorer then
-      return explorer[explorer_method](explorer, ...)
-    end
-  end)
-end
-
 ---Invoke a member's method on the singleton explorer.
 ---Print error when setup not called.
 ---@param explorer_member string explorer member name
@@ -146,7 +146,7 @@ Api.tree.change_root_to_node = wrap_node(function(node)
 end)
 
 Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn)
-Api.tree.get_node_under_cursor = wrap(lib.get_node_at_cursor)
+Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor")
 Api.tree.get_nodes = wrap(lib.get_nodes)
 
 ---@class ApiTreeFindFileOpts
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 6dbbd1720c9..edad77b5d11 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -1,3 +1,4 @@
+local core = require("nvim-tree.core")
 local git = require("nvim-tree.git")
 local log = require("nvim-tree.log")
 local notify = require("nvim-tree.notify")
@@ -386,6 +387,19 @@ function Explorer:get_cursor_position()
   return vim.api.nvim_win_get_cursor(winnr)
 end
 
+---@return Node|nil
+function Explorer:get_node_at_cursor()
+  local cursor = self:get_cursor_position()
+  if not cursor then
+    return
+  end
+
+  if cursor[1] == 1 and view.is_root_folder_visible(core.get_cwd()) then
+    return self
+  end
+
+  return utils.get_nodes_by_line(self.nodes, core.get_nodes_starting_line())[cursor[1]]
+end
 
 function Explorer:setup(opts)
   config = opts
diff --git a/lua/nvim-tree/explorer/live-filter.lua b/lua/nvim-tree/explorer/live-filter.lua
index 30152d63ae4..6ba4b553d6b 100644
--- a/lua/nvim-tree/explorer/live-filter.lua
+++ b/lua/nvim-tree/explorer/live-filter.lua
@@ -196,7 +196,7 @@ local function create_overlay(self)
 end
 
 function LiveFilter:start_filtering()
-  view.View.live_filter.prev_focused_node = require("nvim-tree.lib").get_node_at_cursor()
+  view.View.live_filter.prev_focused_node = self.explorer:get_node_at_cursor()
   self.filter = self.filter or ""
 
   self.explorer.renderer:draw()
@@ -210,7 +210,7 @@ function LiveFilter:start_filtering()
 end
 
 function LiveFilter:clear_filter()
-  local node = require("nvim-tree.lib").get_node_at_cursor()
+  local node = self.explorer:get_node_at_cursor()
   local last_node = view.View.live_filter.prev_focused_node
 
   self.filter = nil
diff --git a/lua/nvim-tree/lib.lua b/lua/nvim-tree/lib.lua
index 2b9ead582aa..1b127e27cbc 100644
--- a/lua/nvim-tree/lib.lua
+++ b/lua/nvim-tree/lib.lua
@@ -1,6 +1,5 @@
 local view = require("nvim-tree.view")
 local core = require("nvim-tree.core")
-local utils = require("nvim-tree.utils")
 local events = require("nvim-tree.events")
 local notify = require("nvim-tree.notify")
 
@@ -13,25 +12,6 @@ local M = {
   target_winid = nil,
 }
 
----@return Node|nil
-function M.get_node_at_cursor()
-  local explorer = core.get_explorer()
-  if not explorer then
-    return
-  end
-
-  local cursor = explorer: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
-  end
-
-  return utils.get_nodes_by_line(explorer.nodes, core.get_nodes_starting_line())[cursor[1]]
-end
-
 ---Api.tree.get_nodes
 ---@return Node[]?
 function M.get_nodes()
diff --git a/lua/nvim-tree/log.lua b/lua/nvim-tree/log.lua
index ad8f34cf175..a0964b3f5ba 100644
--- a/lua/nvim-tree/log.lua
+++ b/lua/nvim-tree/log.lua
@@ -88,14 +88,12 @@ function M.set_inspect_opts(opts)
 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 Node node to be inspected
 ---@param fmt string for string.format
 ---@vararg any arguments for string.format
 function M.node(typ, node, fmt, ...)
   if M.enabled(typ) then
-    node = node or require("nvim-tree.lib").get_node_at_cursor()
     M.raw(typ, string.format("[%s] [%s] %s\n%s\n", os.date("%Y-%m-%d %H:%M:%S"), typ, (fmt or "???"), vim.inspect(node, inspect_opts)), ...)
   end
 end
diff --git a/lua/nvim-tree/marks/init.lua b/lua/nvim-tree/marks/init.lua
index c2da9009ad8..fde1e534f3b 100644
--- a/lua/nvim-tree/marks/init.lua
+++ b/lua/nvim-tree/marks/init.lua
@@ -151,7 +151,7 @@ function Marks:bulk_move()
     return
   end
 
-  local node_at_cursor = lib.get_node_at_cursor()
+  local node_at_cursor = self.explorer:get_node_at_cursor()
   local default_path = core.get_cwd()
 
   if node_at_cursor and node_at_cursor:is(DirectoryNode) then
@@ -190,7 +190,7 @@ end
 ---@private
 ---@param up boolean
 function Marks:navigate(up)
-  local node = lib.get_node_at_cursor()
+  local node = self.explorer:get_node_at_cursor()
   if not node then
     return
   end

From 8994c1e1ef168632eacc5ffe1161737efa7ab802 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 21 Oct 2024 11:53:07 +1100
Subject: [PATCH 08/88] move lib.get_nodes to Explorer

---
 lua/nvim-tree/api.lua           | 3 +--
 lua/nvim-tree/explorer/init.lua | 6 ++++++
 lua/nvim-tree/lib.lua           | 7 -------
 3 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua
index 50fd9f2a4fc..e99d077b9c1 100644
--- a/lua/nvim-tree/api.lua
+++ b/lua/nvim-tree/api.lua
@@ -1,4 +1,3 @@
-local lib = require("nvim-tree.lib")
 local core = require("nvim-tree.core")
 local view = require("nvim-tree.view")
 local utils = require("nvim-tree.utils")
@@ -147,7 +146,7 @@ end)
 
 Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn)
 Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor")
-Api.tree.get_nodes = wrap(lib.get_nodes)
+Api.tree.get_nodes = wrap_explorer("get_nodes")
 
 ---@class ApiTreeFindFileOpts
 ---@field buf string|number|nil
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index edad77b5d11..7535605d43d 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -401,6 +401,12 @@ function Explorer:get_node_at_cursor()
   return utils.get_nodes_by_line(self.nodes, core.get_nodes_starting_line())[cursor[1]]
 end
 
+---Api.tree.get_nodes
+---@return Node
+function Explorer:get_nodes()
+  return self:clone()
+end
+
 function Explorer:setup(opts)
   config = opts
   require("nvim-tree.explorer.watch").setup(opts)
diff --git a/lua/nvim-tree/lib.lua b/lua/nvim-tree/lib.lua
index 1b127e27cbc..8e6902e5f81 100644
--- a/lua/nvim-tree/lib.lua
+++ b/lua/nvim-tree/lib.lua
@@ -12,13 +12,6 @@ local M = {
   target_winid = nil,
 }
 
----Api.tree.get_nodes
----@return Node[]?
-function M.get_nodes()
-  local explorer = core.get_explorer()
-  return explorer and explorer:clone()
-end
-
 function M.set_target_win()
   local id = vim.api.nvim_get_current_win()
   local tree_id = view.get_winnr()

From 5830585318bc16e104dbf050354dcd225fa58821 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 21 Oct 2024 11:56:10 +1100
Subject: [PATCH 09/88] move place_cursor_on_node to Explorer

---
 lua/nvim-tree.lua               | 31 ++++---------------------------
 lua/nvim-tree/explorer/init.lua | 21 +++++++++++++++++++++
 2 files changed, 25 insertions(+), 27 deletions(-)

diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua
index a45c5c6c820..d5cbba60c4b 100644
--- a/lua/nvim-tree.lua
+++ b/lua/nvim-tree.lua
@@ -114,32 +114,6 @@ function M.open_on_directory()
   actions.root.change_dir.force_dirchange(bufname, true)
 end
 
-function M.place_cursor_on_node()
-  local ok, search = pcall(vim.fn.searchcount)
-  if ok and search and search.exact_match == 1 then
-    return
-  end
-
-  local explorer = core.get_explorer()
-  if not explorer then
-    return
-  end
-
-  local node = explorer:get_node_at_cursor()
-  if not node or node.name == ".." then
-    return
-  end
-  node = node:get_parent_of_group() or node
-
-  local line = vim.api.nvim_get_current_line()
-  local cursor = vim.api.nvim_win_get_cursor(0)
-  local idx = vim.fn.stridx(line, node.name)
-
-  if idx >= 0 then
-    vim.api.nvim_win_set_cursor(0, { cursor[1], idx })
-  end
-end
-
 ---@return table
 function M.get_config()
   return M.config
@@ -270,7 +244,10 @@ local function setup_autocommands(opts)
       pattern = "NvimTree_*",
       callback = function()
         if utils.is_nvim_tree_buf(0) then
-          M.place_cursor_on_node()
+          local explorer = core.get_explorer()
+          if explorer then
+            explorer:place_cursor_on_node()
+          end
         end
       end,
     })
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 7535605d43d..54fbb512e04 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -401,6 +401,27 @@ function Explorer:get_node_at_cursor()
   return utils.get_nodes_by_line(self.nodes, core.get_nodes_starting_line())[cursor[1]]
 end
 
+function Explorer:place_cursor_on_node()
+  local ok, search = pcall(vim.fn.searchcount)
+  if ok and search and search.exact_match == 1 then
+    return
+  end
+
+  local node = self:get_node_at_cursor()
+  if not node or node.name == ".." then
+    return
+  end
+  node = node:get_parent_of_group() or node
+
+  local line = vim.api.nvim_get_current_line()
+  local cursor = vim.api.nvim_win_get_cursor(0)
+  local idx = vim.fn.stridx(line, node.name)
+
+  if idx >= 0 then
+    vim.api.nvim_win_set_cursor(0, { cursor[1], idx })
+  end
+end
+
 ---Api.tree.get_nodes
 ---@return Node
 function Explorer:get_nodes()

From e2e6b2b0957380e1437e686f98f47ab2932a2f74 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 21 Oct 2024 13:39:14 +1100
Subject: [PATCH 10/88] resolve resource leak in purge_all_state

---
 lua/nvim-tree.lua | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua
index d5cbba60c4b..42d53c590e3 100644
--- a/lua/nvim-tree.lua
+++ b/lua/nvim-tree.lua
@@ -786,13 +786,16 @@ local function localise_default_opts()
 end
 
 function M.purge_all_state()
-  require("nvim-tree.watcher").purge_watchers()
   view.close_all_tabs()
   view.abandon_all_windows()
-  if core.get_explorer() ~= nil then
+  local explorer = core.get_explorer()
+  if explorer then
     require("nvim-tree.git").purge_state()
+    explorer:destroy()
     core.reset_explorer()
   end
+  -- purge orphaned that were not destroyed by their nodes
+  require("nvim-tree.watcher").purge_watchers()
 end
 
 ---@param conf table|nil

From ff85d8034c220a6b183b291f9ebf4e076b292453 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 21 Oct 2024 15:36:21 +1100
Subject: [PATCH 11/88] move many autocommands into Explorer

---
 lua/nvim-tree.lua                     | 113 +-------------------------
 lua/nvim-tree/explorer/init.lua       | 108 +++++++++++++++++++++++-
 lua/nvim-tree/node/directory-link.lua |   4 +
 lua/nvim-tree/node/file-link.lua      |   4 +
 lua/nvim-tree/node/file.lua           |   4 +
 lua/nvim-tree/node/root.lua           |   4 +
 6 files changed, 124 insertions(+), 113 deletions(-)

diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua
index 42d53c590e3..4390adcf35b 100644
--- a/lua/nvim-tree.lua
+++ b/lua/nvim-tree.lua
@@ -1,5 +1,4 @@
 local log = require("nvim-tree.log")
-local appearance = require("nvim-tree.appearance")
 local view = require("nvim-tree.view")
 local utils = require("nvim-tree.utils")
 local actions = require("nvim-tree.actions")
@@ -151,19 +150,6 @@ local function setup_autocommands(opts)
     vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts))
   end
 
-  -- reset and draw (highlights) when colorscheme is changed
-  create_nvim_tree_autocmd("ColorScheme", {
-    callback = function()
-      appearance.setup()
-      view.reset_winhl()
-
-      local explorer = core.get_explorer()
-      if explorer then
-        explorer.renderer:draw()
-      end
-    end,
-  })
-
   -- prevent new opened file from opening in the same window as nvim-tree
   create_nvim_tree_autocmd("BufWipeout", {
     pattern = "NvimTree_*",
@@ -179,79 +165,9 @@ local function setup_autocommands(opts)
     end,
   })
 
-  create_nvim_tree_autocmd("BufWritePost", {
-    callback = function()
-      if opts.auto_reload_on_write and not opts.filesystem_watchers.enable then
-        local explorer = core.get_explorer()
-        if explorer then
-          explorer:reload_explorer()
-        end
-      end
-    end,
-  })
-
-  create_nvim_tree_autocmd("BufReadPost", {
-    callback = function(data)
-      -- update opened file buffers
-      local explorer = core.get_explorer()
-      if not explorer then
-        return
-      end
-      if
-        (explorer.filters.config.filter_no_buffer or explorer.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
-      then
-        utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
-          explorer:reload_explorer()
-        end)
-      end
-    end,
-  })
-
-  create_nvim_tree_autocmd("BufUnload", {
-    callback = function(data)
-      -- update opened file buffers
-      local explorer = core.get_explorer()
-      if not explorer then
-        return
-      end
-      if
-        (explorer.filters.config.filter_no_buffer or explorer.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
-      then
-        utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
-          explorer:reload_explorer()
-        end)
-      end
-    end,
-  })
-
-  create_nvim_tree_autocmd("User", {
-    pattern = { "FugitiveChanged", "NeogitStatusRefreshed" },
-    callback = function()
-      if not opts.filesystem_watchers.enable and opts.git.enable then
-        local explorer = core.get_explorer()
-        if explorer then
-          explorer:reload_git()
-        end
-      end
-    end,
-  })
-
   if opts.tab.sync.open then
     create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) })
   end
-  if opts.hijack_cursor then
-    create_nvim_tree_autocmd("CursorMoved", {
-      pattern = "NvimTree_*",
-      callback = function()
-        if utils.is_nvim_tree_buf(0) then
-          local explorer = core.get_explorer()
-          if explorer then
-            explorer:place_cursor_on_node()
-          end
-        end
-      end,
-    })
-  end
   if opts.sync_root_with_cwd then
     create_nvim_tree_autocmd("DirChanged", {
       callback = function()
@@ -277,20 +193,6 @@ local function setup_autocommands(opts)
     create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory })
   end
 
-  create_nvim_tree_autocmd("BufEnter", {
-    pattern = "NvimTree_*",
-    callback = function()
-      if utils.is_nvim_tree_buf(0) then
-        if vim.fn.getcwd() ~= core.get_cwd() or (opts.reload_on_bufenter and not opts.filesystem_watchers.enable) then
-          local explorer = core.get_explorer()
-          if explorer then
-            explorer:reload_explorer()
-          end
-        end
-      end
-    end,
-  })
-
   if opts.view.centralize_selection then
     create_nvim_tree_autocmd("BufEnter", {
       pattern = "NvimTree_*",
@@ -330,20 +232,6 @@ local function setup_autocommands(opts)
       end,
     })
   end
-
-  if opts.modified.enable then
-    create_nvim_tree_autocmd({ "BufModifiedSet", "BufWritePost" }, {
-      callback = function()
-        utils.debounce("Buf:modified", opts.view.debounce_delay, function()
-          require("nvim-tree.buffers").reload_modified()
-          local explorer = core.get_explorer()
-          if explorer then
-            explorer:reload_explorer()
-          end
-        end)
-      end,
-    })
-  end
 end
 
 local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
@@ -839,6 +727,7 @@ function M.setup(conf)
   require("nvim-tree.appearance").setup()
   require("nvim-tree.diagnostics").setup(opts)
   require("nvim-tree.explorer"):setup(opts)
+  require("nvim-tree.explorer.watch").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/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 54fbb512e04..02c0152d5e5 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -1,3 +1,5 @@
+local appearance = require("nvim-tree.appearance")
+local buffers = require("nvim-tree.buffers")
 local core = require("nvim-tree.core")
 local git = require("nvim-tree.git")
 local log = require("nvim-tree.log")
@@ -25,7 +27,9 @@ local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
 local config
 
 ---@class (exact) Explorer: RootNode
+---@field uid_explorer number vim.uv.hrtime() at construction time
 ---@field opts table user options
+---@field augroup_id integer
 ---@field renderer Renderer
 ---@field filters Filters
 ---@field live_filter LiveFilter
@@ -59,6 +63,9 @@ function Explorer:create(path)
 
   o.explorer = o
 
+  o.uid_explorer = vim.uv.hrtime()
+  o.augroup_id = vim.api.nvim_create_augroup("NvimTree_Explorer_" .. o.uid_explorer, {})
+
   o.open = true
   o.opts = config
 
@@ -69,11 +76,111 @@ function Explorer:create(path)
   o.marks = Marks:new(config, o)
   o.clipboard = Clipboard:new(config, o)
 
+  o:create_autocmds()
+
   o:_load(o)
 
   return o
 end
 
+function Explorer:destroy()
+  log.line("dev", "Explorer:destroy")
+
+  vim.api.nvim_del_augroup_by_id(self.augroup_id)
+
+  RootNode.destroy(self)
+end
+
+function Explorer:create_autocmds()
+  -- reset and draw (highlights) when colorscheme is changed
+  vim.api.nvim_create_autocmd("ColorScheme", {
+    group = self.augroup_id,
+    callback = function()
+      appearance.setup()
+      view.reset_winhl()
+      self:draw()
+    end,
+  })
+
+  vim.api.nvim_create_autocmd("BufWritePost", {
+    group = self.augroup_id,
+    callback = function()
+      if self.opts.auto_reload_on_write and not self.opts.filesystem_watchers.enable then
+        self:reload_explorer()
+      end
+    end,
+  })
+
+  vim.api.nvim_create_autocmd("BufReadPost", {
+    group = self.augroup_id,
+    callback = function(data)
+      if (self.filters.config.filter_no_buffer or self.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == "" then
+        utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
+          self:reload_explorer()
+        end)
+      end
+    end,
+  })
+
+  -- update opened file buffers
+  vim.api.nvim_create_autocmd("BufUnload", {
+    group = self.augroup_id,
+    callback = function(data)
+      if (self.filters.config.filter_no_buffer or self.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == "" then
+        utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
+          self:reload_explorer()
+        end)
+      end
+    end,
+  })
+
+  vim.api.nvim_create_autocmd("BufEnter", {
+    group = self.augroup_id,
+    pattern = "NvimTree_*",
+    callback = function()
+      if utils.is_nvim_tree_buf(0) then
+        if vim.fn.getcwd() ~= core.get_cwd() or (self.opts.reload_on_bufenter and not self.opts.filesystem_watchers.enable) then
+          self:reload_explorer()
+        end
+      end
+    end,
+  })
+
+  vim.api.nvim_create_autocmd("User", {
+    group = self.augroup_id,
+    pattern = { "FugitiveChanged", "NeogitStatusRefreshed" },
+    callback = function()
+      if not self.opts.filesystem_watchers.enable and self.opts.git.enable then
+        self:reload_git()
+      end
+    end,
+  })
+
+  if self.opts.hijack_cursor then
+    vim.api.nvim_create_autocmd("CursorMoved", {
+      group = self.augroup_id,
+      pattern = "NvimTree_*",
+      callback = function()
+        if utils.is_nvim_tree_buf(0) then
+          self:place_cursor_on_node()
+        end
+      end,
+    })
+  end
+
+  if self.opts.modified.enable then
+    vim.api.nvim_create_autocmd({ "BufModifiedSet", "BufWritePost" }, {
+      group = self.augroup_id,
+      callback = function()
+        utils.debounce("Buf:modified_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
+          buffers.reload_modified()
+          self:reload_explorer()
+        end)
+      end,
+    })
+  end
+end
+
 ---@param node DirectoryNode
 function Explorer:expand(node)
   self:_load(node)
@@ -430,7 +537,6 @@ end
 
 function Explorer:setup(opts)
   config = opts
-  require("nvim-tree.explorer.watch").setup(opts)
 end
 
 return Explorer
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index f0543825fe9..0e6fd338297 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -32,6 +32,10 @@ function DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name
   return o
 end
 
+function DirectoryLinkNode:destroy()
+  DirectoryNode.destroy(self)
+end
+
 -----Update the directory GitStatus of link target and the file status of the link itself
 -----@param parent_ignored boolean
 -----@param status table|nil
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 2bdd79f13f3..2d2571f01a8 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -28,6 +28,10 @@ function FileLinkNode:create(explorer, parent, absolute_path, link_to, name, fs_
   return o
 end
 
+function FileLinkNode:destroy()
+  FileNode.destroy(self)
+end
+
 -----Update the GitStatus of the target otherwise the link itself
 -----@param parent_ignored boolean
 -----@param status table|nil
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 0f01347c0cf..398607c6c9a 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -36,6 +36,10 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat)
   return o
 end
 
+function FileNode:destroy()
+  Node.destroy(self)
+end
+
 ---Update the GitStatus of the file
 ---@param parent_ignored boolean
 ---@param status table|nil
diff --git a/lua/nvim-tree/node/root.lua b/lua/nvim-tree/node/root.lua
index 1b236775e70..2fd037cecf5 100644
--- a/lua/nvim-tree/node/root.lua
+++ b/lua/nvim-tree/node/root.lua
@@ -23,4 +23,8 @@ function RootNode:is_dotfile()
   return false
 end
 
+function RootNode:destroy()
+  DirectoryNode.destroy(self)
+end
+
 return RootNode

From 4c9c8852c7034720aaf17f5b7f88501bb687a03a Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 12:39:39 +1100
Subject: [PATCH 12/88] post merge tidy

---
 lua/nvim-tree/actions/finders/find-file.lua | 1 -
 lua/nvim-tree/actions/moves/item.lua        | 3 +--
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/lua/nvim-tree/actions/finders/find-file.lua b/lua/nvim-tree/actions/finders/find-file.lua
index 34a8ee90d3c..55e2f23d337 100644
--- a/lua/nvim-tree/actions/finders/find-file.lua
+++ b/lua/nvim-tree/actions/finders/find-file.lua
@@ -45,7 +45,6 @@ function M.fn(path)
       return node.absolute_path == path_real or node.link_to == path_real
     end)
     :applier(function(node)
-      ---@cast node DirectoryNode
       local incremented_line = false
       if not node.group_next then
         line = line + 1
diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index 1cc6a3e5e7a..4390fa47564 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -74,8 +74,7 @@ end
 
 ---@param node DirectoryNode
 local function expand_node(node)
-  if node:is(DirectoryNode) and not node.open then
-    ---@cast node DirectoryNode
+  if not node.open then
     -- Expand the node.
     -- Should never collapse since we checked open.
     node:expand_or_collapse(false)

From e115199c5f28a96fa84c81783af5cd6b06928ad2 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 14:55:00 +1100
Subject: [PATCH 13/88] chore: resolve undefined-field

---
 lua/nvim-tree/class.lua          |  5 +++++
 lua/nvim-tree/node/directory.lua | 13 +++++++++++++
 lua/nvim-tree/node/init.lua      | 20 +++-----------------
 3 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/lua/nvim-tree/class.lua b/lua/nvim-tree/class.lua
index 8565e3c6ded..41f7f9b69b0 100644
--- a/lua/nvim-tree/class.lua
+++ b/lua/nvim-tree/class.lua
@@ -37,4 +37,9 @@ function Class:as(class)
   return self:is(class) and self or nil
 end
 
+-- avoid unused param warnings in abstract methods
+---@param ... any
+function Class:nop(...)
+end
+
 return Class
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index cf423364a74..da4bcef9a8f 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -139,6 +139,19 @@ function DirectoryNode:refresh()
   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)
+    local dir = node:as(DirectoryNode)
+    if dir and #dir.nodes > 0 then
+      dir:reload_node_status(projects)
+    end
+  end
+end
+
 -- If node is grouped, return the last node in the group. Otherwise, return the given node.
 ---@return DirectoryNode
 function DirectoryNode:last_group_node()
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index 5b7f7b1a601..d11df48b7e4 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -21,32 +21,18 @@ local Node = Class:new()
 function Node:destroy()
 end
 
---luacheck: push ignore 212
 ---Update the GitStatus of the node
+---Abstract
 ---@param parent_ignored boolean
 ---@param status table?
-function Node:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local
-  ---TODO find a way to declare abstract methods
+function Node:update_git_status(parent_ignored, status)
+  self:nop(parent_ignored, status)
 end
 
---luacheck: pop
-
 ---@return GitStatus?
 function Node:get_git_status()
 end
 
----@param projects table
-function Node: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
-      node:reload_node_status(projects)
-    end
-  end
-end
-
 ---@return boolean
 function Node:is_git_ignored()
   return self.git_status ~= nil and self.git_status.file == "!!"

From dd937d017f4d3ddd50e1b2749abf3a2b321178a4 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 14:57:01 +1100
Subject: [PATCH 14/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/decorator/diagnostics.lua | 3 ++-
 lua/nvim-tree/renderer/decorator/hidden.lua      | 4 +++-
 lua/nvim-tree/renderer/decorator/modified.lua    | 3 ++-
 3 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/renderer/decorator/diagnostics.lua b/lua/nvim-tree/renderer/decorator/diagnostics.lua
index 3daee7bc03a..ef59ff70e03 100644
--- a/lua/nvim-tree/renderer/decorator/diagnostics.lua
+++ b/lua/nvim-tree/renderer/decorator/diagnostics.lua
@@ -4,6 +4,7 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION
 local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
 
 local Decorator = require("nvim-tree.renderer.decorator")
+local DirectoryNode = require("nvim-tree.node.directory")
 
 -- highlight groups by severity
 local HG_ICON = {
@@ -98,7 +99,7 @@ function DecoratorDiagnostics:calculate_highlight(node)
   end
 
   local group
-  if node.nodes then
+  if node:is(DirectoryNode) then
     group = HG_FOLDER[diag_value]
   else
     group = HG_FILE[diag_value]
diff --git a/lua/nvim-tree/renderer/decorator/hidden.lua b/lua/nvim-tree/renderer/decorator/hidden.lua
index 1df68c48295..000d84461c1 100644
--- a/lua/nvim-tree/renderer/decorator/hidden.lua
+++ b/lua/nvim-tree/renderer/decorator/hidden.lua
@@ -1,6 +1,8 @@
 local HL_POSITION = require("nvim-tree.enum").HL_POSITION
 local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
+
 local Decorator = require("nvim-tree.renderer.decorator")
+local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class (exact) DecoratorHidden: Decorator
 ---@field icon HighlightedString?
@@ -48,7 +50,7 @@ function DecoratorHidden:calculate_highlight(node)
     return nil
   end
 
-  if node.nodes then
+  if node:is(DirectoryNode) then
     return "NvimTreeHiddenFolderHL"
   else
     return "NvimTreeHiddenFileHL"
diff --git a/lua/nvim-tree/renderer/decorator/modified.lua b/lua/nvim-tree/renderer/decorator/modified.lua
index 4665343f0ee..0cd2d55c191 100644
--- a/lua/nvim-tree/renderer/decorator/modified.lua
+++ b/lua/nvim-tree/renderer/decorator/modified.lua
@@ -4,6 +4,7 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION
 local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
 
 local Decorator = require("nvim-tree.renderer.decorator")
+local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class (exact) DecoratorModified: Decorator
 ---@field icon HighlightedString|nil
@@ -55,7 +56,7 @@ function DecoratorModified:calculate_highlight(node)
     return nil
   end
 
-  if node.nodes then
+  if node:is(DirectoryNode) then
     return "NvimTreeModifiedFolderHL"
   else
     return "NvimTreeModifiedFileHL"

From 64e0c9018362b97a8b99d6345784fa3c821795c1 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 15:13:22 +1100
Subject: [PATCH 15/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/decorator/git.lua | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/lua/nvim-tree/renderer/decorator/git.lua b/lua/nvim-tree/renderer/decorator/git.lua
index af2c8ccaa94..c6e24672391 100644
--- a/lua/nvim-tree/renderer/decorator/git.lua
+++ b/lua/nvim-tree/renderer/decorator/git.lua
@@ -4,15 +4,19 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION
 local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
 
 local Decorator = require("nvim-tree.renderer.decorator")
+local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class HighlightedStringGit: HighlightedString
 ---@field ord number decreasing priority
 
+---@alias IconsByStatus table<string, HighlightedStringGit[]> by human status
+---@alias IconsByXY table<string, HighlightedStringGit[]> by porcelain status
+
 ---@class (exact) DecoratorGit: Decorator
 ---@field file_hl table<string, string>? by porcelain status e.g. "AM"
 ---@field folder_hl table<string, string>? by porcelain status
----@field icons_by_status HighlightedStringGit[]? by human status
----@field icons_by_xy table<string, HighlightedStringGit[]>? by porcelain status
+---@field icons_by_status IconsByStatus?
+---@field icons_by_xy IconsByXY?
 local DecoratorGit = Decorator:new()
 
 ---Static factory method
@@ -49,7 +53,7 @@ function DecoratorGit:create(opts, explorer)
   return o
 end
 
----@param glyphs table<string, string> user glyps
+---@param glyphs IconsByStatus user glyps
 function DecoratorGit:build_icons_by_status(glyphs)
   self.icons_by_status = {
     staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 },
@@ -62,7 +66,7 @@ function DecoratorGit:build_icons_by_status(glyphs)
   }
 end
 
----@param icons HighlightedStringGit[]
+---@param icons IconsByXY
 function DecoratorGit:build_icons_by_xy(icons)
   self.icons_by_xy = {
     ["M "] = { icons.staged },
@@ -214,7 +218,7 @@ function DecoratorGit:calculate_highlight(node)
     return nil
   end
 
-  if node.nodes then
+  if node:is(DirectoryNode) then
     return self.folder_hl[git_status[1]]
   else
     return self.file_hl[git_status[1]]

From 6aba4a8a163d4d51840af99e12cbb235bddd1dc6 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 15:18:58 +1100
Subject: [PATCH 16/88] chore: resolve undefined-field

---
 lua/nvim-tree/utils.lua | 28 +++++++++++++++++++++-------
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua
index 936982ec0a7..a27f6daf60e 100644
--- a/lua/nvim-tree/utils.lua
+++ b/lua/nvim-tree/utils.lua
@@ -1,6 +1,8 @@
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local notify = require("nvim-tree.notify")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {
   debouncers = {},
 }
@@ -124,7 +126,12 @@ function M.find_node(nodes, fn)
   local node, i = Iterator.builder(nodes)
     :matcher(fn)
     :recursor(function(node)
-      return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
+      local dir = node:as(DirectoryNode)
+      if dir then
+        return dir.group_next and { dir.group_next } or (dir.open and #dir.nodes > 0 and dir.nodes)
+      else
+        return false
+      end
     end)
     :iterate()
   i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
@@ -179,11 +186,12 @@ function M.get_node_from_path(path)
       return node.absolute_path == path or node.link_to == path
     end)
     :recursor(function(node)
-      if node.group_next then
-        return { node.group_next }
+      local dir = node:as(DirectoryNode)
+      if dir and dir.group_next then
+        return { dir.group_next }
       end
-      if node.nodes then
-        return node.nodes
+      if dir then
+        return dir.nodes
       end
     end)
     :iterate()
@@ -219,14 +227,20 @@ function M.get_nodes_by_line(nodes_all, line_start)
 
   Iterator.builder(nodes_all)
     :applier(function(node)
-      if node.group_next then
+      local dir = node:as(DirectoryNode)
+      if dir and dir.group_next then
         return
       end
       nodes_by_line[line] = node
       line = line + 1
     end)
     :recursor(function(node)
-      return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
+      local dir = node:as(DirectoryNode)
+      if dir then
+        return dir.group_next and { dir.group_next } or (dir.open and #dir.nodes > 0 and dir.nodes)
+      else
+        return false
+      end
     end)
     :iterate()
 

From 7548dfdcd2d5192660b62d13926f246665b12d57 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 15:22:44 +1100
Subject: [PATCH 17/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index b16cc58320f..0517362cb65 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -4,6 +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")
+local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class GitStatus
 ---@field file string|nil
@@ -201,6 +202,7 @@ local function reload_tree_at(toplevel)
 
   log.line("watcher", "git event executing '%s'", toplevel)
   local root_node = utils.get_node_from_path(toplevel)
+  root_node = root_node and root_node:as(DirectoryNode)
   if not root_node then
     return
   end
@@ -215,7 +217,8 @@ local function reload_tree_at(toplevel)
         node:update_git_status(parent_ignored, git_status)
       end)
       :recursor(function(node)
-        return node.nodes and #node.nodes > 0 and node.nodes
+        local dir = node:as(DirectoryNode)
+        return dir and #dir.nodes > 0 and dir.nodes
       end)
       :iterate()
 

From e7b9c71f8f404d6c842fca2122af52c4fcf09696 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 15:24:16 +1100
Subject: [PATCH 18/88] chore: resolve undefined-field

---
 lua/nvim-tree/marks/init.lua | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lua/nvim-tree/marks/init.lua b/lua/nvim-tree/marks/init.lua
index fde1e534f3b..ddeb5cc9200 100644
--- a/lua/nvim-tree/marks/init.lua
+++ b/lua/nvim-tree/marks/init.lua
@@ -200,7 +200,8 @@ function Marks:navigate(up)
 
   Iterator.builder(self.explorer.nodes)
     :recursor(function(n)
-      return n.open and n.nodes
+      local dir = n:as(DirectoryNode)
+      return dir and dir.open and dir.nodes
     end)
     :applier(function(n)
       if n.absolute_path == node.absolute_path then
@@ -263,7 +264,7 @@ function Marks:navigate_select()
       return
     end
     local node = self.marks[choice]
-    if node and not node.nodes and not utils.get_win_buf_from_path(node.absolute_path) then
+    if node and not node:is(DirectoryNode) and not utils.get_win_buf_from_path(node.absolute_path) then
       open_file.fn("edit", node.absolute_path)
     elseif node then
       utils.focus_file(node.absolute_path)

From e9e4bcdd18f3cead736db0ad07b5bfcaaa3c851e Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 15:26:07 +1100
Subject: [PATCH 19/88] chore: resolve undefined-field

---
 lua/nvim-tree/diagnostics.lua | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/lua/nvim-tree/diagnostics.lua b/lua/nvim-tree/diagnostics.lua
index 05845ba274b..a5cc5413b7e 100644
--- a/lua/nvim-tree/diagnostics.lua
+++ b/lua/nvim-tree/diagnostics.lua
@@ -3,6 +3,8 @@ local utils = require("nvim-tree.utils")
 local view = require("nvim-tree.view")
 local log = require("nvim-tree.log")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 
 ---COC severity level strings to LSP severity levels
@@ -125,7 +127,7 @@ end
 local function from_cache(node)
   local nodepath = uniformize_path(node.absolute_path)
   local max_severity = nil
-  if not node.nodes then
+  if not node:is(DirectoryNode) then
     -- direct cache hit for files
     max_severity = NODE_SEVERITIES[nodepath]
   else
@@ -184,7 +186,7 @@ function M.get_diag_status(node)
   end
 
   -- dir but we shouldn't show on dirs at all
-  if node.nodes ~= nil and not M.show_on_dirs then
+  if node:is(DirectoryNode) and not M.show_on_dirs then
     return nil
   end
 
@@ -195,13 +197,15 @@ function M.get_diag_status(node)
     node.diag_status = from_cache(node)
   end
 
+  local dir = node:as(DirectoryNode)
+
   -- file
-  if not node.nodes then
+  if not dir then
     return node.diag_status
   end
 
   -- dir is closed or we should show on open_dirs
-  if not node.open or M.show_on_open_dirs then
+  if not dir.open or M.show_on_open_dirs then
     return node.diag_status
   end
   return nil

From 4ba0db7ec9c11ad3cb839ab17c2b08f4e76cac89 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 18:17:47 +1100
Subject: [PATCH 20/88] Revert "chore: resolve undefined-field"

This reverts commit be546ff18d41f28466b065c857e1e041659bd2c8.
---
 lua/nvim-tree/utils.lua | 28 +++++++---------------------
 1 file changed, 7 insertions(+), 21 deletions(-)

diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua
index a27f6daf60e..936982ec0a7 100644
--- a/lua/nvim-tree/utils.lua
+++ b/lua/nvim-tree/utils.lua
@@ -1,8 +1,6 @@
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local notify = require("nvim-tree.notify")
 
-local DirectoryNode = require("nvim-tree.node.directory")
-
 local M = {
   debouncers = {},
 }
@@ -126,12 +124,7 @@ function M.find_node(nodes, fn)
   local node, i = Iterator.builder(nodes)
     :matcher(fn)
     :recursor(function(node)
-      local dir = node:as(DirectoryNode)
-      if dir then
-        return dir.group_next and { dir.group_next } or (dir.open and #dir.nodes > 0 and dir.nodes)
-      else
-        return false
-      end
+      return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
     end)
     :iterate()
   i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
@@ -186,12 +179,11 @@ function M.get_node_from_path(path)
       return node.absolute_path == path or node.link_to == path
     end)
     :recursor(function(node)
-      local dir = node:as(DirectoryNode)
-      if dir and dir.group_next then
-        return { dir.group_next }
+      if node.group_next then
+        return { node.group_next }
       end
-      if dir then
-        return dir.nodes
+      if node.nodes then
+        return node.nodes
       end
     end)
     :iterate()
@@ -227,20 +219,14 @@ function M.get_nodes_by_line(nodes_all, line_start)
 
   Iterator.builder(nodes_all)
     :applier(function(node)
-      local dir = node:as(DirectoryNode)
-      if dir and dir.group_next then
+      if node.group_next then
         return
       end
       nodes_by_line[line] = node
       line = line + 1
     end)
     :recursor(function(node)
-      local dir = node:as(DirectoryNode)
-      if dir then
-        return dir.group_next and { dir.group_next } or (dir.open and #dir.nodes > 0 and dir.nodes)
-      else
-        return false
-      end
+      return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
     end)
     :iterate()
 

From 1e188927c5d0b4955a8a0740ecf241b9fd4d99c4 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 18:18:39 +1100
Subject: [PATCH 21/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 0517362cb65..7ec8f396860 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -4,7 +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")
-local DirectoryNode = require("nvim-tree.node.directory")
+local DirectoryNode = nil -- circular dependency
 
 ---@class GitStatus
 ---@field file string|nil
@@ -352,6 +352,7 @@ end
 function M.setup(opts)
   M.config.git = opts.git
   M.config.filesystem_watchers = opts.filesystem_watchers
+  DirectoryNode = require("nvim-tree.node.directory")
 end
 
 return M

From 2a98d56c3ee1a68e93b68b94e110a4e89dd5740f Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 19:32:57 +1100
Subject: [PATCH 22/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua   |  32 ++++----
 lua/nvim-tree/git/runner.lua | 138 ++++++++++++++++++++---------------
 lua/nvim-tree/git/utils.lua  |   3 +-
 3 files changed, 97 insertions(+), 76 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 7ec8f396860..7fd03a020ca 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -1,7 +1,7 @@
 local log = require("nvim-tree.log")
 local utils = require("nvim-tree.utils")
 local git_utils = require("nvim-tree.git.utils")
-local Runner = require("nvim-tree.git.runner")
+local runner = require("nvim-tree.git.runner")
 local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = nil -- circular dependency
@@ -36,17 +36,17 @@ local WATCHED_FILES = {
 ---@param toplevel string|nil
 ---@param path string|nil
 ---@param project table
----@param git_status table|nil
-local function reload_git_status(toplevel, path, project, git_status)
+---@param statuses GitStatusesXYByPath?
+local function reload_git_statuses(toplevel, path, project, statuses)
   if path then
     for p in pairs(project.files) do
       if p:find(path, 1, true) == 1 then
         project.files[p] = nil
       end
     end
-    project.files = vim.tbl_deep_extend("force", project.files, git_status)
+    project.files = vim.tbl_deep_extend("force", project.files, statuses)
   else
-    project.files = git_status
+    project.files = statuses
   end
 
   project.dirs = git_utils.file_status_to_dir_status(project.files, toplevel)
@@ -105,7 +105,8 @@ function M.reload_project(toplevel, path, callback)
     return
   end
 
-  local opts = {
+  ---@type RunnerOpts
+  local runner_opts = {
     toplevel = toplevel,
     path = path,
     list_untracked = git_utils.should_show_untracked(toplevel),
@@ -114,14 +115,15 @@ function M.reload_project(toplevel, path, callback)
   }
 
   if callback then
-    Runner.run(opts, function(git_status)
-      reload_git_status(toplevel, path, project, git_status)
+    ---@param statuses GitStatusesXYByPath
+    runner_opts.callback = function(statuses)
+      reload_git_statuses(toplevel, path, project, statuses)
       callback()
-    end)
+    end
+    runner(runner_opts)
   else
     -- TODO #1974 use callback once async/await is available
-    local git_status = Runner.run(opts)
-    reload_git_status(toplevel, path, project, git_status)
+    reload_git_statuses(toplevel, path, project, runner(runner_opts))
   end
 end
 
@@ -246,7 +248,7 @@ function M.load_project_status(path)
     return status
   end
 
-  local git_status = Runner.run({
+  local statuses = runner({
     toplevel = toplevel,
     list_untracked = git_utils.should_show_untracked(toplevel),
     list_ignored = true,
@@ -273,10 +275,10 @@ function M.load_project_status(path)
     })
   end
 
-  if git_status then
+  if statuses then
     M._projects_by_toplevel[toplevel] = {
-      files = git_status,
-      dirs = git_utils.file_status_to_dir_status(git_status, toplevel),
+      files = statuses,
+      dirs = git_utils.file_status_to_dir_status(statuses, toplevel),
       watcher = watcher,
     }
     return M._projects_by_toplevel[toplevel]
diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua
index 06235ea5e64..1a9000f933d 100644
--- a/lua/nvim-tree/git/runner.lua
+++ b/lua/nvim-tree/git/runner.lua
@@ -2,9 +2,23 @@ local log = require("nvim-tree.log")
 local utils = require("nvim-tree.utils")
 local notify = require("nvim-tree.notify")
 
----@class Runner
-local Runner = {}
-Runner.__index = Runner
+local Class = require("nvim-tree.class")
+
+---@alias GitStatusesXYByPath table<string, string>
+
+---@class (exact) RunnerOpts
+---@field toplevel string absolute path
+---@field path string? absolute path
+---@field list_untracked boolean
+---@field list_ignored boolean
+---@field timeout integer
+---@field callback fun(statuses: GitStatusesXYByPath)?
+
+---@class (exact) Runner: Class
+---@field opts RunnerOpts
+---@field statuses GitStatusesXYByPath
+---@field rc integer? -- -1 indicates timeout
+local Runner = Class:new()
 
 local timeouts = 0
 local MAX_TIMEOUTS = 5
@@ -12,7 +26,7 @@ local MAX_TIMEOUTS = 5
 ---@private
 ---@param status string
 ---@param path string|nil
-function Runner:_parse_status_output(status, path)
+function Runner:parse_status_output(status, path)
   if not path then
     return
   end
@@ -22,7 +36,7 @@ function Runner:_parse_status_output(status, path)
     path = path:gsub("/", "\\")
   end
   if #status > 0 and #path > 0 then
-    self.output[utils.path_remove_trailing(utils.path_join({ self.toplevel, path }))] = status
+    self.statuses[utils.path_remove_trailing(utils.path_join({ self.opts.toplevel, path }))] = status
   end
 end
 
@@ -30,7 +44,7 @@ end
 ---@param prev_output string
 ---@param incoming string
 ---@return string
-function Runner:_handle_incoming_data(prev_output, incoming)
+function Runner:handle_incoming_data(prev_output, incoming)
   if incoming and utils.str_find(incoming, "\n") then
     local prev = prev_output .. incoming
     local i = 1
@@ -45,7 +59,7 @@ function Runner:_handle_incoming_data(prev_output, incoming)
           -- skip next line if it is a rename entry
           skip_next_line = true
         end
-        self:_parse_status_output(status, path)
+        self:parse_status_output(status, path)
       end
       i = i + #line
     end
@@ -58,35 +72,38 @@ function Runner:_handle_incoming_data(prev_output, incoming)
   end
 
   for line in prev_output:gmatch("[^\n]*\n") do
-    self:_parse_status_output(line)
+    self:parse_status_output(line)
   end
 
   return ""
 end
 
+---@private
 ---@param stdout_handle uv.uv_pipe_t
 ---@param stderr_handle uv.uv_pipe_t
----@return table
-function Runner:_getopts(stdout_handle, stderr_handle)
-  local untracked = self.list_untracked and "-u" or nil
-  local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no"
+---@return uv.spawn.options
+function Runner:get_spawn_options(stdout_handle, stderr_handle)
+  local untracked = self.opts.list_untracked and "-u" or nil
+  local ignored = (self.opts.list_untracked and self.opts.list_ignored) and "--ignored=matching" or "--ignored=no"
   return {
-    args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path },
-    cwd = self.toplevel,
+    args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.opts.path },
+    cwd = self.opts.toplevel,
     stdio = { nil, stdout_handle, stderr_handle },
   }
 end
 
+---@private
 ---@param output string
-function Runner:_log_raw_output(output)
+function Runner:log_raw_output(output)
   if log.enabled("git") and output and type(output) == "string" then
     log.raw("git", "%s", output)
     log.line("git", "done")
   end
 end
 
+---@private
 ---@param callback function|nil
-function Runner:_run_git_job(callback)
+function Runner:run_git_job(callback)
   local handle, pid
   local stdout = vim.loop.new_pipe(false)
   local stderr = vim.loop.new_pipe(false)
@@ -123,20 +140,20 @@ function Runner:_run_git_job(callback)
     end
   end
 
-  local opts = self:_getopts(stdout, stderr)
-  log.line("git", "running job with timeout %dms", self.timeout)
-  log.line("git", "git %s",                        table.concat(utils.array_remove_nils(opts.args), " "))
+  local spawn_options = self:get_spawn_options(stdout, stderr)
+  log.line("git", "running job with timeout %dms", self.opts.timeout)
+  log.line("git", "git %s",                        table.concat(utils.array_remove_nils(spawn_options.args), " "))
 
   handle, pid = vim.loop.spawn(
     "git",
-    opts,
+    spawn_options,
     vim.schedule_wrap(function(rc)
       on_finish(rc)
     end)
   )
 
   timer:start(
-    self.timeout,
+    self.opts.timeout,
     0,
     vim.schedule_wrap(function()
       on_finish(-1)
@@ -151,19 +168,20 @@ function Runner:_run_git_job(callback)
     if data then
       data = data:gsub("%z", "\n")
     end
-    self:_log_raw_output(data)
-    output_leftover = self:_handle_incoming_data(output_leftover, data)
+    self:log_raw_output(data)
+    output_leftover = self:handle_incoming_data(output_leftover, data)
   end
 
   local function manage_stderr(_, data)
-    self:_log_raw_output(data)
+    self:log_raw_output(data)
   end
 
   vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout))
   vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr))
 end
 
-function Runner:_wait()
+---@private
+function Runner:wait()
   local function is_done()
     return self.rc ~= nil
   end
@@ -172,64 +190,64 @@ function Runner:_wait()
   end
 end
 
----@param opts table
-function Runner:_finalise(opts)
+---@private
+function Runner:finalise()
   if self.rc == -1 then
-    log.line("git", "job timed out  %s %s", opts.toplevel, opts.path)
+    log.line("git", "job timed out  %s %s", self.opts.toplevel, self.opts.path)
     timeouts = timeouts + 1
     if timeouts == MAX_TIMEOUTS then
-      notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts, opts.timeout))
+      notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts,
+        self.opts.timeout))
       require("nvim-tree.git").disable_git_integration()
     end
   elseif self.rc ~= 0 then
-    log.line("git", "job fail rc %d %s %s", self.rc, opts.toplevel, opts.path)
+    log.line("git", "job fail rc %d %s %s", self.rc, self.opts.toplevel, self.opts.path)
   else
-    log.line("git", "job success    %s %s", opts.toplevel, opts.path)
+    log.line("git", "job success    %s %s", self.opts.toplevel, self.opts.path)
   end
 end
 
---- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
----@param opts table
----@param callback function|nil executed passing return when complete
----@return table|nil status by absolute path, nil if callback present
-function Runner.run(opts, callback)
-  local self = setmetatable({
-    toplevel = opts.toplevel,
-    path = opts.path,
-    list_untracked = opts.list_untracked,
-    list_ignored = opts.list_ignored,
-    timeout = opts.timeout or 400,
-    output = {},
-    rc = nil, -- -1 indicates timeout
-  }, Runner)
-
-  local async = callback ~= nil
-  local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.toplevel, opts.path)
-
-  if async and callback then
+---@return GitStatusesXYByPath? statuses nil if callback present
+function Runner:run()
+  local async = self.opts.callback ~= nil
+  local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.opts.toplevel, self.opts.path)
+
+  if async and self.opts.callback then
     -- async, always call back
-    self:_run_git_job(function()
+    self:run_git_job(function()
       log.profile_end(profile)
 
-      self:_finalise(opts)
+      self:finalise()
 
-      callback(self.output)
+      self.opts.callback(self.statuses)
     end)
   else
     -- sync, maybe call back
-    self:_run_git_job()
-    self:_wait()
+    self:run_git_job()
+    self:wait()
 
     log.profile_end(profile)
 
-    self:_finalise(opts)
+    self:finalise()
 
-    if callback then
-      callback(self.output)
+    if self.opts.callback then
+      self.opts.callback(self.statuses)
     else
-      return self.output
+      return self.statuses
     end
   end
 end
 
-return Runner
+---Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
+---@param opts RunnerOpts
+---@return GitStatusesXYByPath? statuses nil if callback present
+return function(opts)
+  ---@type Runner
+  local runner = {
+    opts = opts,
+    statuses = {},
+  }
+  runner = Runner:new(runner) --[[@as Runner]]
+
+  return runner:run()
+end
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 6a32b632e31..8caaf73af6e 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -58,10 +58,11 @@ function M.get_toplevel(cwd)
   return toplevel, git_dir
 end
 
+---@type table<string, boolean>
 local untracked = {}
 
 ---@param cwd string
----@return string|nil
+---@return boolean
 function M.should_show_untracked(cwd)
   if untracked[cwd] ~= nil then
     return untracked[cwd]

From ab4f769f82e1d83d471316f47293e9fcee6ba660 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Fri, 25 Oct 2024 19:48:28 +1100
Subject: [PATCH 23/88] chore: resolve undefined-field

---
 lua/nvim-tree/class.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/nvim-tree/class.lua b/lua/nvim-tree/class.lua
index 41f7f9b69b0..a43cbd8af54 100644
--- a/lua/nvim-tree/class.lua
+++ b/lua/nvim-tree/class.lua
@@ -39,7 +39,7 @@ end
 
 -- avoid unused param warnings in abstract methods
 ---@param ... any
-function Class:nop(...)
+function Class:nop(...) --luacheck: ignore 212
 end
 
 return Class

From 1c812aed124e40ca0aed3b0245338d4ea0b14789 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 09:57:55 +1100
Subject: [PATCH 24/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua   | 25 +++++++++---------
 lua/nvim-tree/git/runner.lua | 50 +++++++++++++++++++-----------------
 2 files changed, 39 insertions(+), 36 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 7fd03a020ca..22dd70e413b 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -1,7 +1,8 @@
 local log = require("nvim-tree.log")
 local utils = require("nvim-tree.utils")
 local git_utils = require("nvim-tree.git.utils")
-local runner = require("nvim-tree.git.runner")
+
+local GitRunner = require("nvim-tree.git.runner")
 local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = nil -- circular dependency
@@ -36,7 +37,7 @@ local WATCHED_FILES = {
 ---@param toplevel string|nil
 ---@param path string|nil
 ---@param project table
----@param statuses GitStatusesXYByPath?
+---@param statuses GitXYByPath?
 local function reload_git_statuses(toplevel, path, project, statuses)
   if path then
     for p in pairs(project.files) do
@@ -105,7 +106,7 @@ function M.reload_project(toplevel, path, callback)
     return
   end
 
-  ---@type RunnerOpts
+  ---@type GitRunnerOpts
   local runner_opts = {
     toplevel = toplevel,
     path = path,
@@ -115,15 +116,15 @@ function M.reload_project(toplevel, path, callback)
   }
 
   if callback then
-    ---@param statuses GitStatusesXYByPath
+    ---@param statuses GitXYByPath
     runner_opts.callback = function(statuses)
       reload_git_statuses(toplevel, path, project, statuses)
       callback()
     end
-    runner(runner_opts)
+    GitRunner:run(runner_opts)
   else
     -- TODO #1974 use callback once async/await is available
-    reload_git_statuses(toplevel, path, project, runner(runner_opts))
+    reload_git_statuses(toplevel, path, project, GitRunner:run(runner_opts))
   end
 end
 
@@ -203,16 +204,16 @@ local function reload_tree_at(toplevel)
   end
 
   log.line("watcher", "git event executing '%s'", toplevel)
-  local root_node = utils.get_node_from_path(toplevel)
-  root_node = root_node and root_node:as(DirectoryNode)
-  if not root_node then
+  local base = utils.get_node_from_path(toplevel)
+  base = base and base:as(DirectoryNode)
+  if not base then
     return
   end
 
   M.reload_project(toplevel, nil, function()
     local git_status = M.get_project(toplevel)
 
-    Iterator.builder(root_node.nodes)
+    Iterator.builder(base.nodes)
       :hidden()
       :applier(function(node)
         local parent_ignored = node.parent and node.parent:is_git_ignored() or false
@@ -224,7 +225,7 @@ local function reload_tree_at(toplevel)
       end)
       :iterate()
 
-    root_node.explorer.renderer:draw()
+    base.explorer.renderer:draw()
   end)
 end
 
@@ -248,7 +249,7 @@ function M.load_project_status(path)
     return status
   end
 
-  local statuses = runner({
+  local statuses = GitRunner:run({
     toplevel = toplevel,
     list_untracked = git_utils.should_show_untracked(toplevel),
     list_ignored = true,
diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua
index 1a9000f933d..d4163905549 100644
--- a/lua/nvim-tree/git/runner.lua
+++ b/lua/nvim-tree/git/runner.lua
@@ -4,21 +4,21 @@ local notify = require("nvim-tree.notify")
 
 local Class = require("nvim-tree.class")
 
----@alias GitStatusesXYByPath table<string, string>
+---@alias GitXYByPath table<string, string> -- short-format statuses
 
----@class (exact) RunnerOpts
+---@class (exact) GitRunnerOpts
 ---@field toplevel string absolute path
 ---@field path string? absolute path
 ---@field list_untracked boolean
 ---@field list_ignored boolean
 ---@field timeout integer
----@field callback fun(statuses: GitStatusesXYByPath)?
+---@field callback fun(statuses: GitXYByPath)?
 
----@class (exact) Runner: Class
----@field opts RunnerOpts
----@field statuses GitStatusesXYByPath
----@field rc integer? -- -1 indicates timeout
-local Runner = Class:new()
+---@class (exact) GitRunner: Class
+---@field private opts GitRunnerOpts
+---@field private statuses GitXYByPath
+---@field private rc integer? -- -1 indicates timeout
+local GitRunner = Class:new()
 
 local timeouts = 0
 local MAX_TIMEOUTS = 5
@@ -26,7 +26,7 @@ local MAX_TIMEOUTS = 5
 ---@private
 ---@param status string
 ---@param path string|nil
-function Runner:parse_status_output(status, path)
+function GitRunner:parse_status_output(status, path)
   if not path then
     return
   end
@@ -44,7 +44,7 @@ end
 ---@param prev_output string
 ---@param incoming string
 ---@return string
-function Runner:handle_incoming_data(prev_output, incoming)
+function GitRunner:handle_incoming_data(prev_output, incoming)
   if incoming and utils.str_find(incoming, "\n") then
     local prev = prev_output .. incoming
     local i = 1
@@ -82,7 +82,7 @@ end
 ---@param stdout_handle uv.uv_pipe_t
 ---@param stderr_handle uv.uv_pipe_t
 ---@return uv.spawn.options
-function Runner:get_spawn_options(stdout_handle, stderr_handle)
+function GitRunner:get_spawn_options(stdout_handle, stderr_handle)
   local untracked = self.opts.list_untracked and "-u" or nil
   local ignored = (self.opts.list_untracked and self.opts.list_ignored) and "--ignored=matching" or "--ignored=no"
   return {
@@ -94,7 +94,7 @@ end
 
 ---@private
 ---@param output string
-function Runner:log_raw_output(output)
+function GitRunner:log_raw_output(output)
   if log.enabled("git") and output and type(output) == "string" then
     log.raw("git", "%s", output)
     log.line("git", "done")
@@ -103,7 +103,7 @@ end
 
 ---@private
 ---@param callback function|nil
-function Runner:run_git_job(callback)
+function GitRunner:run_git_job(callback)
   local handle, pid
   local stdout = vim.loop.new_pipe(false)
   local stderr = vim.loop.new_pipe(false)
@@ -181,7 +181,7 @@ function Runner:run_git_job(callback)
 end
 
 ---@private
-function Runner:wait()
+function GitRunner:wait()
   local function is_done()
     return self.rc ~= nil
   end
@@ -191,7 +191,7 @@ function Runner:wait()
 end
 
 ---@private
-function Runner:finalise()
+function GitRunner:finalise()
   if self.rc == -1 then
     log.line("git", "job timed out  %s %s", self.opts.toplevel, self.opts.path)
     timeouts = timeouts + 1
@@ -207,8 +207,8 @@ function Runner:finalise()
   end
 end
 
----@return GitStatusesXYByPath? statuses nil if callback present
-function Runner:run()
+---@return GitXYByPath? statuses nil if callback present
+function GitRunner:execute()
   local async = self.opts.callback ~= nil
   local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.opts.toplevel, self.opts.path)
 
@@ -238,16 +238,18 @@ function Runner:run()
   end
 end
 
----Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
----@param opts RunnerOpts
----@return GitStatusesXYByPath? statuses nil if callback present
-return function(opts)
-  ---@type Runner
+---Static method to run a git process, which will be killed if it takes more than timeout
+---@param opts GitRunnerOpts
+---@return GitXYByPath? statuses nil if callback present
+function GitRunner:run(opts)
+  ---@type GitRunner
   local runner = {
     opts = opts,
     statuses = {},
   }
-  runner = Runner:new(runner) --[[@as Runner]]
+  runner = GitRunner:new(runner) --[[@as GitRunner]]
 
-  return runner:run()
+  return runner:execute()
 end
+
+return GitRunner

From 179c2e25521bf38b27a7d24f570a339668b113bd Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 10:17:32 +1100
Subject: [PATCH 25/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/builder.lua | 1 +
 lua/nvim-tree/renderer/init.lua    | 2 --
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua
index 9804bfcc4a0..104abb08dc2 100644
--- a/lua/nvim-tree/renderer/builder.lua
+++ b/lua/nvim-tree/renderer/builder.lua
@@ -45,6 +45,7 @@ local PICTURE_MAP = {
 ---@field extmarks table[] extra marks for right icon placement
 ---@field virtual_lines table[] virtual lines for hidden count display
 ---@field private explorer Explorer
+---@field private opts table
 ---@field private index number
 ---@field private depth number
 ---@field private combined_groups table<string, boolean> combined group names
diff --git a/lua/nvim-tree/renderer/init.lua b/lua/nvim-tree/renderer/init.lua
index e222e709b30..49a69d1b162 100644
--- a/lua/nvim-tree/renderer/init.lua
+++ b/lua/nvim-tree/renderer/init.lua
@@ -16,7 +16,6 @@ local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtua
 ---@field private __index? table
 ---@field private opts table user options
 ---@field private explorer Explorer
----@field private builder Builder
 local Renderer = {}
 
 ---@param opts table user options
@@ -27,7 +26,6 @@ function Renderer:new(opts, explorer)
   local o = {
     opts = opts,
     explorer = explorer,
-    builder = Builder:new(opts, explorer),
   }
 
   setmetatable(o, self)

From 8128fedfd33eb573f2324bbb3bac2d0b3d68123c Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 11:44:38 +1100
Subject: [PATCH 26/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/decorator/git.lua | 53 ++++++++++++------------
 1 file changed, 27 insertions(+), 26 deletions(-)

diff --git a/lua/nvim-tree/renderer/decorator/git.lua b/lua/nvim-tree/renderer/decorator/git.lua
index c6e24672391..422a3b53117 100644
--- a/lua/nvim-tree/renderer/decorator/git.lua
+++ b/lua/nvim-tree/renderer/decorator/git.lua
@@ -6,17 +6,19 @@ local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
 local Decorator = require("nvim-tree.renderer.decorator")
 local DirectoryNode = require("nvim-tree.node.directory")
 
----@class HighlightedStringGit: HighlightedString
+---@class (exact) GitHighlightedString: HighlightedString
 ---@field ord number decreasing priority
 
----@alias IconsByStatus table<string, HighlightedStringGit[]> by human status
----@alias IconsByXY table<string, HighlightedStringGit[]> by porcelain status
+---@alias GitStatuses "deleted"|"ignored"|"renamed"|"staged"|"unmerged"|"unstaged"|"untracked"
+---@alias GitIconsByStatus table<GitStatuses, GitHighlightedString> human status
+---@alias GitIconsByXY table<string, GitHighlightedString[]> porcelain status
+---@alias GitGlyphs table<GitStatuses, string> from opts
 
 ---@class (exact) DecoratorGit: Decorator
----@field file_hl table<string, string>? by porcelain status e.g. "AM"
----@field folder_hl table<string, string>? by porcelain status
----@field icons_by_status IconsByStatus?
----@field icons_by_xy IconsByXY?
+---@field file_hl_by_xy table<string, string>?
+---@field folder_hl_by_xy table<string, string>?
+---@field icons_by_status GitIconsByStatus?
+---@field icons_by_xy GitIconsByXY?
 local DecoratorGit = Decorator:new()
 
 ---Static factory method
@@ -38,7 +40,7 @@ function DecoratorGit:create(opts, explorer)
   end
 
   if o.hl_pos ~= HL_POSITION.none then
-    o:build_hl_table()
+    o:build_file_folder_hl_by_xy()
   end
 
   if opts.renderer.icons.show.git then
@@ -53,20 +55,19 @@ function DecoratorGit:create(opts, explorer)
   return o
 end
 
----@param glyphs IconsByStatus user glyps
+---@param glyphs GitGlyphs
 function DecoratorGit:build_icons_by_status(glyphs)
-  self.icons_by_status = {
-    staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 },
-    unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 },
-    renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 },
-    deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 },
-    unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 },
-    untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 },
-    ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 },
-  }
+  self.icons_by_status = {}
+  self.icons_by_status.staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }
+  self.icons_by_status.unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }
+  self.icons_by_status.renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 }
+  self.icons_by_status.deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 }
+  self.icons_by_status.unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 }
+  self.icons_by_status.untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 }
+  self.icons_by_status.ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 }
 end
 
----@param icons IconsByXY
+---@param icons GitIconsByXY
 function DecoratorGit:build_icons_by_xy(icons)
   self.icons_by_xy = {
     ["M "] = { icons.staged },
@@ -104,8 +105,8 @@ function DecoratorGit:build_icons_by_xy(icons)
   }
 end
 
-function DecoratorGit:build_hl_table()
-  self.file_hl = {
+function DecoratorGit:build_file_folder_hl_by_xy()
+  self.file_hl_by_xy = {
     ["M "] = "NvimTreeGitFileStagedHL",
     ["C "] = "NvimTreeGitFileStagedHL",
     ["AA"] = "NvimTreeGitFileStagedHL",
@@ -138,9 +139,9 @@ function DecoratorGit:build_hl_table()
     [" A"] = "none",
   }
 
-  self.folder_hl = {}
-  for k, v in pairs(self.file_hl) do
-    self.folder_hl[k] = v:gsub("File", "Folder")
+  self.folder_hl_by_xy = {}
+  for k, v in pairs(self.file_hl_by_xy) do
+    self.folder_hl_by_xy[k] = v:gsub("File", "Folder")
   end
 end
 
@@ -219,9 +220,9 @@ function DecoratorGit:calculate_highlight(node)
   end
 
   if node:is(DirectoryNode) then
-    return self.folder_hl[git_status[1]]
+    return self.folder_hl_by_xy[git_status[1]]
   else
-    return self.file_hl[git_status[1]]
+    return self.file_hl_by_xy[git_status[1]]
   end
 end
 

From 7fdca3067326e56b131e51df5762e7a1045a1df1 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 12:00:01 +1100
Subject: [PATCH 27/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/decorator/git.lua | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/renderer/decorator/git.lua b/lua/nvim-tree/renderer/decorator/git.lua
index 422a3b53117..67ba1ad1773 100644
--- a/lua/nvim-tree/renderer/decorator/git.lua
+++ b/lua/nvim-tree/renderer/decorator/git.lua
@@ -9,10 +9,11 @@ local DirectoryNode = require("nvim-tree.node.directory")
 ---@class (exact) GitHighlightedString: HighlightedString
 ---@field ord number decreasing priority
 
----@alias GitStatuses "deleted"|"ignored"|"renamed"|"staged"|"unmerged"|"unstaged"|"untracked"
----@alias GitIconsByStatus table<GitStatuses, GitHighlightedString> human status
+---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked"
+
+---@alias GitIconsByStatus table<GitStatusStrings, GitHighlightedString> human status
 ---@alias GitIconsByXY table<string, GitHighlightedString[]> porcelain status
----@alias GitGlyphs table<GitStatuses, string> from opts
+---@alias GitGlyphs table<GitStatusStrings, string> from opts
 
 ---@class (exact) DecoratorGit: Decorator
 ---@field file_hl_by_xy table<string, string>?

From e61b16d615b6a05cddd5011b2d63107f336a3f49 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 12:08:03 +1100
Subject: [PATCH 28/88] chore: resolve undefined-field

---
 lua/nvim-tree/node/directory.lua | 2 +-
 lua/nvim-tree/node/file.lua      | 2 +-
 lua/nvim-tree/node/init.lua      | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index da4bcef9a8f..7c3246fa194 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -73,7 +73,7 @@ function DirectoryNode:update_git_status(parent_ignored, status)
   self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path, nil)
 end
 
----@return GitStatus|nil
+---@return string[]? xy short-format statuses
 function DirectoryNode:get_git_status()
   if not self.git_status or not self.explorer.opts.git.show_on_dirs then
     return nil
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 398607c6c9a..5b9394e9c62 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -47,7 +47,7 @@ function FileNode:update_git_status(parent_ignored, status)
   self.git_status = git.git_status_file(parent_ignored, status, self.absolute_path, nil)
 end
 
----@return GitStatus|nil
+---@return string[]? xy short-format statuses
 function FileNode:get_git_status()
   if not self.git_status then
     return nil
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index d11df48b7e4..f084cd4705a 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -29,7 +29,7 @@ function Node:update_git_status(parent_ignored, status)
   self:nop(parent_ignored, status)
 end
 
----@return GitStatus?
+---@return string[]? xy short-format statuses
 function Node:get_git_status()
 end
 

From 3d0012804140fbb554244662e7bdacee5253978a Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 12:10:39 +1100
Subject: [PATCH 29/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 22dd70e413b..38ab078a65a 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -7,9 +7,9 @@ local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = nil -- circular dependency
 
----@class GitStatus
----@field file string|nil
----@field dir table|nil
+---@class GitStatus -- xy short-format statuses
+---@field file string?
+---@field dir string[]?
 
 local M = {
   config = {},

From 568d0f15aba9227005438e0586a8166d6630b59d Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 12:43:19 +1100
Subject: [PATCH 30/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/init.lua       |  2 +-
 lua/nvim-tree/explorer/watch.lua      |  3 +-
 lua/nvim-tree/git/init.lua            | 92 ++++++++++++++++-----------
 lua/nvim-tree/git/utils.lua           | 42 ++++++++++++
 lua/nvim-tree/node/directory-link.lua |  4 +-
 lua/nvim-tree/node/directory.lua      | 36 +----------
 lua/nvim-tree/node/file-link.lua      |  4 +-
 lua/nvim-tree/node/file.lua           |  4 +-
 lua/nvim-tree/node/init.lua           | 34 ----------
 9 files changed, 110 insertions(+), 111 deletions(-)

diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index ac958a44824..0bad7fb126b 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -478,7 +478,7 @@ function Explorer:reload_git()
   event_running = true
 
   local projects = git.reload()
-  self:reload_node_status(projects)
+  git.reload_node_status(self, projects)
   self.renderer:draw()
   event_running = false
 end
diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua
index a83758132ca..be37bf663e3 100644
--- a/lua/nvim-tree/explorer/watch.lua
+++ b/lua/nvim-tree/explorer/watch.lua
@@ -1,4 +1,5 @@
 local log = require("nvim-tree.log")
+local git = require("nvim-tree.git")
 local utils = require("nvim-tree.utils")
 local Watcher = require("nvim-tree.watcher").Watcher
 
@@ -76,7 +77,7 @@ function M.create_watcher(node)
       else
         log.line("watcher", "node event executing refresh '%s'", node.absolute_path)
       end
-      node:refresh()
+      git.refresh_dir(node)
     end)
   end
 
diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 38ab078a65a..43c9279b564 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -5,7 +5,7 @@ local git_utils = require("nvim-tree.git.utils")
 local GitRunner = require("nvim-tree.git.runner")
 local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
-local DirectoryNode = nil -- circular dependency
+local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class GitStatus -- xy short-format statuses
 ---@field file string?
@@ -289,46 +289,67 @@ 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 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, path, path_file)
-  if parent_ignored then
-    return { file = "!!" }
-  end
+---@param dir DirectoryNode
+---@param project table?
+---@param root string?
+function M.update_parent_statuses(dir, project, root)
+  while project and dir do
+    -- step up to the containing project
+    if dir.absolute_path == root then
+      -- stop at the top of the tree
+      if not dir.parent then
+        break
+      end
 
-  if status then
-    return {
-      file = status.files and (status.files[path] or status.files[path_file]),
-      dir = status.dirs and {
-        direct = status.dirs.direct and status.dirs.direct[path],
-        indirect = status.dirs.indirect and status.dirs.indirect[path],
-      },
-    }
+      root = M.get_toplevel(dir.parent.absolute_path)
+
+      -- stop when no more projects
+      if not root then
+        break
+      end
+
+      -- update the containing project
+      project = M.get_project(root)
+      M.reload_project(root, dir.absolute_path, nil)
+    end
+
+    -- update status
+    dir:update_git_status(dir.parent and dir.parent:is_git_ignored() or false, project)
+
+    -- maybe parent
+    dir = dir.parent
   end
 end
 
----Git file status for an absolute path with optional fallback
----@param parent_ignored boolean
----@param status table|nil
----@param path string
----@param path_fallback string?
----@return GitStatus
-function M.git_status_file(parent_ignored, status, path, path_fallback)
-  if parent_ignored then
-    return { file = "!!" }
-  end
+---Refresh contents and git status for a single directory
+---@param dir DirectoryNode
+function M.refresh_dir(dir)
+  local node = dir:get_parent_of_group() or dir
+  local toplevel = M.get_toplevel(dir.absolute_path)
 
-  if not status or not status.files then
-    return {}
-  end
+  M.reload_project(toplevel, dir.absolute_path, function()
+    local project = M.get_project(toplevel) or {}
 
-  return {
-    file = status.files[path] or status.files[path_fallback]
-  }
+    dir.explorer:reload(node, project)
+
+    M.update_parent_statuses(dir, project, toplevel)
+
+    dir.explorer.renderer:draw()
+  end)
+end
+
+---@param n DirectoryNode
+---@param projects table
+function M.reload_node_status(n, projects)
+  local toplevel = M.get_toplevel(n.absolute_path)
+  local status = projects[toplevel] or {}
+  for _, node in ipairs(n.nodes) do
+    node:update_git_status(n:is_git_ignored(), status)
+    local dir = node:as(DirectoryNode)
+    if dir and #dir.nodes > 0 then
+      dir:reload_node_status(projects)
+    end
+  end
 end
 
 function M.purge_state()
@@ -355,7 +376,6 @@ end
 function M.setup(opts)
   M.config.git = opts.git
   M.config.filesystem_watchers = opts.filesystem_watchers
-  DirectoryNode = require("nvim-tree.node.directory")
 end
 
 return M
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 8caaf73af6e..75f1db5cb23 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -127,6 +127,48 @@ function M.file_status_to_dir_status(status, cwd)
   return r
 end
 
+---Git file status for an absolute path with optional fallback
+---@param parent_ignored boolean
+---@param status table|nil
+---@param path string
+---@param path_fallback string?
+---@return GitStatus
+function M.git_status_file(parent_ignored, status, path, path_fallback)
+  if parent_ignored then
+    return { file = "!!" }
+  end
+
+  if not status or not status.files then
+    return {}
+  end
+
+  return {
+    file = status.files[path] or status.files[path_fallback]
+  }
+end
+
+---Git file and directory status for an absolute path with optional file fallback
+---@param parent_ignored boolean
+---@param status table|nil
+---@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, path, path_file)
+  if parent_ignored then
+    return { file = "!!" }
+  end
+
+  if status then
+    return {
+      file = status.files and (status.files[path] or status.files[path_file]),
+      dir = status.dirs and {
+        direct = status.dirs.direct and status.dirs.direct[path],
+        indirect = status.dirs.indirect and status.dirs.indirect[path],
+      },
+    }
+  end
+end
+
 function M.setup(opts)
   if opts.git.cygwin_support then
     M.use_cygpath = vim.fn.executable("cygpath") == 1
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 0e6fd338297..210daddb516 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -1,4 +1,4 @@
-local git = require("nvim-tree.git")
+local git_utils = require("nvim-tree.git.utils")
 
 local DirectoryNode = require("nvim-tree.node.directory")
 
@@ -40,7 +40,7 @@ end
 -----@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.absolute_path)
+  self.git_status = git_utils.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 7c3246fa194..a04c017fa95 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,5 +1,4 @@
-local git = require("nvim-tree.git")
-local watch = require("nvim-tree.explorer.watch")
+local git_utils = require("nvim-tree.git.utils")
 
 local Node = require("nvim-tree.node")
 
@@ -46,7 +45,7 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat)
   }
   o = self:new(o) --[[@as DirectoryNode]]
 
-  o.watcher = watch.create_watcher(o)
+  o.watcher = require("nvim-tree.explorer.watch").create_watcher(o)
 
   return o
 end
@@ -70,7 +69,7 @@ end
 ---@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, nil)
+  self.git_status = git_utils.git_status_dir(parent_ignored, status, self.absolute_path, nil)
 end
 
 ---@return string[]? xy short-format statuses
@@ -123,35 +122,6 @@ function DirectoryNode:get_git_status()
   end
 end
 
----Refresh contents and git status for a single node
-function DirectoryNode:refresh()
-  local node = self:get_parent_of_group() or self
-  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(node, project)
-
-    node:update_parent_statuses(project, toplevel)
-
-    self.explorer.renderer:draw()
-  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)
-    local dir = node:as(DirectoryNode)
-    if dir and #dir.nodes > 0 then
-      dir:reload_node_status(projects)
-    end
-  end
-end
-
 -- If node is grouped, return the last node in the group. Otherwise, return the given node.
 ---@return DirectoryNode
 function DirectoryNode:last_group_node()
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 2d2571f01a8..ecefb771576 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -1,4 +1,4 @@
-local git = require("nvim-tree.git")
+local git_utils = require("nvim-tree.git.utils")
 
 local FileNode = require("nvim-tree.node.file")
 
@@ -36,7 +36,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_utils.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 5b9394e9c62..cbf83a6f6c3 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -1,4 +1,4 @@
-local git = require("nvim-tree.git")
+local git_utils = require("nvim-tree.git.utils")
 local utils = require("nvim-tree.utils")
 
 local Node = require("nvim-tree.node")
@@ -44,7 +44,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, nil)
+  self.git_status = git_utils.git_status_file(parent_ignored, status, self.absolute_path, nil)
 end
 
 ---@return string[]? xy short-format statuses
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index f084cd4705a..6d999b0fbf7 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -1,5 +1,3 @@
-local git = require("nvim-tree.git")
-
 local Class = require("nvim-tree.class")
 
 ---Abstract Node class.
@@ -52,38 +50,6 @@ function Node:is_dotfile()
   return false
 end
 
----@param project table?
----@param root string?
-function Node: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
-
 ---Get the highest parent of grouped nodes, nil when not grouped
 ---@return DirectoryNode?
 function Node:get_parent_of_group()

From 3218d96114ec9a5654355444249a73b8b34f1691 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 12:52:40 +1100
Subject: [PATCH 31/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 43c9279b564..c60c124af69 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -338,17 +338,19 @@ function M.refresh_dir(dir)
   end)
 end
 
----@param n DirectoryNode
+---@param dir DirectoryNode?
 ---@param projects table
-function M.reload_node_status(n, projects)
-  local toplevel = M.get_toplevel(n.absolute_path)
+function M.reload_node_status(dir, projects)
+  dir = dir and dir:as(DirectoryNode)
+  if not dir or #dir.nodes == 0 then
+    return
+  end
+
+  local toplevel = M.get_toplevel(dir.absolute_path)
   local status = projects[toplevel] or {}
-  for _, node in ipairs(n.nodes) do
-    node:update_git_status(n:is_git_ignored(), status)
-    local dir = node:as(DirectoryNode)
-    if dir and #dir.nodes > 0 then
-      dir:reload_node_status(projects)
-    end
+  for _, node in ipairs(dir.nodes) do
+    node:update_git_status(dir:is_git_ignored(), status)
+    M.reload_node_status(node:as(DirectoryNode), projects)
   end
 end
 

From 868a5ef75179d5419ea239d24bda46dfca7b3761 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 13:06:13 +1100
Subject: [PATCH 32/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index c60c124af69..8c1bdaf8eec 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -9,7 +9,7 @@ local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class GitStatus -- xy short-format statuses
 ---@field file string?
----@field dir string[]?
+---@field dir table<"direct" | "indirect", string[]>?
 
 local M = {
   config = {},

From d631ee1ebb954c83b59b44d7e3b36d90f4daab27 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 13:19:47 +1100
Subject: [PATCH 33/88] chore: resolve undefined-field

---
 lua/nvim-tree/buffers.lua       | 6 ++++--
 lua/nvim-tree/explorer/init.lua | 2 +-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/buffers.lua b/lua/nvim-tree/buffers.lua
index 954c7e4011c..c026887627e 100644
--- a/lua/nvim-tree/buffers.lua
+++ b/lua/nvim-tree/buffers.lua
@@ -1,3 +1,5 @@
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 
 ---@type table<string, boolean> record of which file is modified
@@ -27,8 +29,8 @@ function M.is_modified(node)
   return node
     and M.config.modified.enable
     and M._modified[node.absolute_path]
-    and (not node.nodes or M.config.modified.show_on_dirs)
-    and (not node.open or M.config.modified.show_on_open_dirs)
+    and (not node:is(DirectoryNode) or M.config.modified.show_on_dirs)
+    and (not (node:is(DirectoryNode) and node.open) or M.config.modified.show_on_open_dirs)
 end
 
 ---A buffer exists for the node's absolute path
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 0bad7fb126b..88e9048c5a3 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -321,7 +321,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)
+    git.update_parent_statuses(node, project, toplevel)
   end
 
   log.profile_end(profile)

From bb23127cdd736c8a72c3e883a800bc8cfc1605e8 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 13:44:03 +1100
Subject: [PATCH 34/88] chore: resolve undefined-field

---
 lua/nvim-tree/view.lua | 28 +++++++++++++++++++++++-----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/lua/nvim-tree/view.lua b/lua/nvim-tree/view.lua
index a39df75a684..34aaaf2819c 100644
--- a/lua/nvim-tree/view.lua
+++ b/lua/nvim-tree/view.lua
@@ -147,12 +147,30 @@ end
 
 local function set_window_options_and_buffer()
   pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr())
-  local eventignore = vim.opt.eventignore:get()
-  vim.opt.eventignore = "all"
-  for k, v in pairs(M.View.winopts) do
-    vim.opt_local[k] = v
+
+  if vim.fn.has("nvim-0.10") == 1 then
+
+    local eventignore = vim.api.nvim_get_option_value("eventignore", {})
+    vim.api.nvim_set_option_value("eventignore", "all", {})
+
+    for k, v in pairs(M.View.winopts) do
+      vim.api.nvim_set_option_value(k, v, { scope = "local" })
+    end
+
+    vim.api.nvim_set_option_value("eventignore", eventignore, {})
+
+  else
+
+    local eventignore = vim.api.nvim_get_option("eventignore") ---@diagnostic disable-line: deprecated
+    vim.api.nvim_set_option("eventignore", "all") ---@diagnostic disable-line: deprecated
+
+    for k, v in pairs(M.View.winopts) do
+      vim.api.nvim_win_set_option(0, k, v) ---@diagnostic disable-line: deprecated
+    end
+
+    vim.api.nvim_set_option("eventignore", eventignore) ---@diagnostic disable-line: deprecated
+
   end
-  vim.opt.eventignore = eventignore
 end
 
 ---@return table

From d19f68b17099dbea09c5c6b551fc4362999fd5f9 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 13:47:24 +1100
Subject: [PATCH 35/88] chore: resolve undefined-field

---
 lua/nvim-tree/buffers.lua | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lua/nvim-tree/buffers.lua b/lua/nvim-tree/buffers.lua
index c026887627e..7c03e406030 100644
--- a/lua/nvim-tree/buffers.lua
+++ b/lua/nvim-tree/buffers.lua
@@ -26,11 +26,12 @@ end
 ---@param node Node
 ---@return boolean
 function M.is_modified(node)
+  local dir = node:as(DirectoryNode)
   return node
     and M.config.modified.enable
     and M._modified[node.absolute_path]
-    and (not node:is(DirectoryNode) or M.config.modified.show_on_dirs)
-    and (not (node:is(DirectoryNode) and node.open) or M.config.modified.show_on_open_dirs)
+    and (not dir or M.config.modified.show_on_dirs)
+    and (not (dir and dir.open) or M.config.modified.show_on_open_dirs)
 end
 
 ---A buffer exists for the node's absolute path

From 9311cfbe8f7adfa5c4f6c1bc55d4ada4699925db Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 13:59:41 +1100
Subject: [PATCH 36/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/moves/item.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index 4390fa47564..edd4b5c385b 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -3,6 +3,7 @@ local view = require("nvim-tree.view")
 local core = require("nvim-tree.core")
 local diagnostics = require("nvim-tree.diagnostics")
 
+local FileNode = require("nvim-tree.node.file")
 local DirectoryNode = require("nvim-tree.node.directory")
 
 local M = {}
@@ -175,7 +176,7 @@ local function move_prev_recursive(explorer, what, skip_gitignored)
       if
         node_cur == nil
         or node_cur == node_init -- we didn't move
-        or not node_cur.nodes    -- node is a file
+        or node_cur:is(FileNode) -- node is a file
       then
         return
       end

From e0d737dae9ff38151b0bba8c4231e16b79488aad Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 14:01:09 +1100
Subject: [PATCH 37/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/components/diagnostics.lua | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/renderer/components/diagnostics.lua b/lua/nvim-tree/renderer/components/diagnostics.lua
index e51712e7746..e91873e07ca 100644
--- a/lua/nvim-tree/renderer/components/diagnostics.lua
+++ b/lua/nvim-tree/renderer/components/diagnostics.lua
@@ -1,6 +1,8 @@
 local HL_POSITION = require("nvim-tree.enum").HL_POSITION
 local diagnostics = require("nvim-tree.diagnostics")
 
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {
   -- highlight strings for the icons
   HS_ICON = {},
@@ -24,7 +26,7 @@ function M.get_highlight(node)
 
   local group
   local diag_status = diagnostics.get_diag_status(node)
-  if node.nodes then
+  if node:is(DirectoryNode) then
     group = M.HS_FOLDER[diag_status and diag_status.value]
   else
     group = M.HS_FILE[diag_status and diag_status.value]

From 52ff301e076d541abb9a5e98289716fe4ee173d3 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 14:02:37 +1100
Subject: [PATCH 38/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/components/padding.lua | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/lua/nvim-tree/renderer/components/padding.lua b/lua/nvim-tree/renderer/components/padding.lua
index fa34c23e13c..349438be827 100644
--- a/lua/nvim-tree/renderer/components/padding.lua
+++ b/lua/nvim-tree/renderer/components/padding.lua
@@ -1,3 +1,5 @@
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {}
 
 local function check_siblings_for_folder(node, with_arrows)
@@ -90,8 +92,9 @@ function M.get_arrows(node)
   local str
   local hl = "NvimTreeFolderArrowClosed"
 
-  if node.nodes then
-    if node.open then
+  local dir = node:as(DirectoryNode)
+  if dir then
+    if dir.open then
       str = M.config.icons.glyphs.folder["arrow_open"] .. " "
       hl = "NvimTreeFolderArrowOpen"
     else

From d663059c3e419e6597fc0aa4430f725a278ee1ee Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 14:55:46 +1100
Subject: [PATCH 39/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/watch.lua |   7 +-
 lua/nvim-tree/git/init.lua       |   9 +-
 lua/nvim-tree/watcher.lua        | 173 ++++++++++++++++++-------------
 3 files changed, 109 insertions(+), 80 deletions(-)

diff --git a/lua/nvim-tree/explorer/watch.lua b/lua/nvim-tree/explorer/watch.lua
index be37bf663e3..bd65ae53607 100644
--- a/lua/nvim-tree/explorer/watch.lua
+++ b/lua/nvim-tree/explorer/watch.lua
@@ -66,9 +66,10 @@ function M.create_watcher(node)
     return nil
   end
 
+  ---@param watcher Watcher
   local function callback(watcher)
-    log.line("watcher", "node event scheduled refresh %s", watcher.context)
-    utils.debounce(watcher.context, M.config.filesystem_watchers.debounce_delay, function()
+    log.line("watcher", "node event scheduled refresh %s", watcher.data.context)
+    utils.debounce(watcher.data.context, M.config.filesystem_watchers.debounce_delay, function()
       if watcher.destroyed then
         return
       end
@@ -82,7 +83,7 @@ function M.create_watcher(node)
   end
 
   M.uid = M.uid + 1
-  return Watcher:new(path, nil, callback, {
+  return Watcher:create(path, nil, callback, {
     context = "explorer:watch:" .. path .. ":" .. M.uid,
   })
 end
diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 8c1bdaf8eec..e0333e456e5 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -260,18 +260,19 @@ function M.load_project_status(path)
   if M.config.filesystem_watchers.enable then
     log.line("watcher", "git start")
 
+    ---@param w Watcher
     local callback = function(w)
-      log.line("watcher", "git event scheduled '%s'", w.toplevel)
-      utils.debounce("git:watcher:" .. w.toplevel, M.config.filesystem_watchers.debounce_delay, function()
+      log.line("watcher", "git event scheduled '%s'", w.data.toplevel)
+      utils.debounce("git:watcher:" .. w.data.toplevel, M.config.filesystem_watchers.debounce_delay, function()
         if w.destroyed then
           return
         end
-        reload_tree_at(w.toplevel)
+        reload_tree_at(w.data.toplevel)
       end)
     end
 
     local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join({ toplevel, ".git" })
-    watcher = Watcher:new(git_dir, WATCHED_FILES, callback, {
+    watcher = Watcher:create(git_dir, WATCHED_FILES, callback, {
       toplevel = toplevel,
     })
   end
diff --git a/lua/nvim-tree/watcher.lua b/lua/nvim-tree/watcher.lua
index cad762e3dde..2b59a0cdf85 100644
--- a/lua/nvim-tree/watcher.lua
+++ b/lua/nvim-tree/watcher.lua
@@ -2,21 +2,7 @@ local notify = require("nvim-tree.notify")
 local log = require("nvim-tree.log")
 local utils = require("nvim-tree.utils")
 
-local M = {
-  config = {},
-}
-
----@class Event
-local Event = {
-  _events = {},
-}
-Event.__index = Event
-
----@class Watcher
-local Watcher = {
-  _watchers = {},
-}
-Watcher.__index = Watcher
+local Class = require("nvim-tree.class")
 
 local FS_EVENT_FLAGS = {
   -- inotify or equivalent will be used; fallback to stat has not yet been implemented
@@ -25,20 +11,40 @@ local FS_EVENT_FLAGS = {
   recursive = false,
 }
 
+local M = {
+  config = {},
+}
+
+---@class (exact) Event: Class
+---@field destroyed boolean
+---@field private path string
+---@field private fs_event uv.uv_fs_event_t?
+---@field private listeners function[]
+local Event = Class:new()
+
+---Registry of all events
+---@type Event[]
+local events = {}
+
+---Static factory method
+---Creates and starts an Event
 ---@param path string
 ---@return Event|nil
-function Event:new(path)
-  log.line("watcher", "Event:new '%s'", path)
-
-  local e = setmetatable({
-    _path = path,
-    _fs_event = nil,
-    _listeners = {},
-  }, Event)
-
-  if e:start() then
-    Event._events[path] = e
-    return e
+function Event:create(path)
+  log.line("watcher", "Event:create '%s'", path)
+
+  ---@type Event
+  local o = {
+    destroyed = false,
+    path = path,
+    fs_event = nil,
+    listeners = {},
+  }
+  o = self:new(o) --[[@as Event]]
+
+  if o:start() then
+    events[path] = o
+    return o
   else
     return nil
   end
@@ -46,21 +52,21 @@ end
 
 ---@return boolean
 function Event:start()
-  log.line("watcher", "Event:start '%s'", self._path)
+  log.line("watcher", "Event:start '%s'", self.path)
 
   local rc, _, name
 
-  self._fs_event, _, name = vim.loop.new_fs_event()
-  if not self._fs_event then
-    self._fs_event = nil
-    notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name))
+  self.fs_event, _, name = vim.loop.new_fs_event()
+  if not self.fs_event then
+    self.fs_event = nil
+    notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self.path, name))
     return false
   end
 
   local event_cb = vim.schedule_wrap(function(err, filename)
     if err then
-      log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self._path, filename, err)
-      local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self._path)
+      log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self.path, filename, err)
+      local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self.path)
       if err == "EPERM" and (utils.is_windows or utils.is_wsl) then
         -- on directory removal windows will cascade the filesystem events out of order
         log.line("watcher", message)
@@ -69,19 +75,19 @@ function Event:start()
         self:destroy(message)
       end
     else
-      log.line("watcher", "event_cb '%s' '%s'", self._path, filename)
-      for _, listener in ipairs(self._listeners) do
+      log.line("watcher", "event_cb '%s' '%s'", self.path, filename)
+      for _, listener in ipairs(self.listeners) do
         listener(filename)
       end
     end
   end)
 
-  rc, _, name = self._fs_event:start(self._path, FS_EVENT_FLAGS, event_cb)
+  rc, _, name = self.fs_event:start(self.path, FS_EVENT_FLAGS, event_cb)
   if rc ~= 0 then
     if name == "EMFILE" then
       M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting")
     else
-      notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self._path, name))
+      notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self.path, name))
     end
     return false
   end
@@ -91,81 +97,102 @@ end
 
 ---@param listener function
 function Event:add(listener)
-  table.insert(self._listeners, listener)
+  table.insert(self.listeners, listener)
 end
 
 ---@param listener function
 function Event:remove(listener)
-  utils.array_remove(self._listeners, listener)
-  if #self._listeners == 0 then
+  utils.array_remove(self.listeners, listener)
+  if #self.listeners == 0 then
     self:destroy()
   end
 end
 
 ---@param message string|nil
 function Event:destroy(message)
-  log.line("watcher", "Event:destroy '%s'", self._path)
+  log.line("watcher", "Event:destroy '%s'", self.path)
 
-  if self._fs_event then
+  if self.fs_event then
     if message then
       notify.warn(message)
     end
 
-    local rc, _, name = self._fs_event:stop()
+    local rc, _, name = self.fs_event:stop()
     if rc ~= 0 then
-      notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self._path, name))
+      notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self.path, name))
     end
-    self._fs_event = nil
+    self.fs_event = nil
   end
 
-  Event._events[self._path] = nil
-
   self.destroyed = true
+  events[self.path] = nil
 end
 
+---Static factory method
+---Creates and starts a Watcher
+---@class (exact) Watcher: Class
+---@field data table user data
+---@field destroyed boolean
+---@field private path string
+---@field private callback fun(watcher: Watcher)
+---@field private files string[]?
+---@field private listener fun(filename: string)?
+---@field private event Event
+local Watcher = Class:new()
+
+---Registry of all watchers
+---@type Watcher[]
+local watchers = {}
+
+---Static factory method
 ---@param path string
 ---@param files string[]|nil
----@param callback function
----@param data table
+---@param callback fun(watcher: Watcher)
+---@param data table user data
 ---@return Watcher|nil
-function Watcher:new(path, files, callback, data)
-  log.line("watcher", "Watcher:new '%s' %s", path, vim.inspect(files))
-
-  local w = setmetatable(data, Watcher)
+function Watcher:create(path, files, callback, data)
+  log.line("watcher", "Watcher:create '%s' %s", path, vim.inspect(files))
 
-  w._event = Event._events[path] or Event:new(path)
-  w._listener = nil
-  w._path = path
-  w._files = files
-  w._callback = callback
-
-  if not w._event then
+  local event = events[path] or Event:create(path)
+  if not event then
     return nil
   end
 
-  w:start()
+  ---@type Watcher
+  local o = {
+    data = data,
+    destroyed = false,
+    path = path,
+    callback = callback,
+    files = files,
+    listener = nil,
+    event = event,
+  }
+  o = self:new(o) --[[@as Watcher]]
+
+  o:start()
 
-  table.insert(Watcher._watchers, w)
+  table.insert(watchers, o)
 
-  return w
+  return o
 end
 
 function Watcher:start()
-  self._listener = function(filename)
-    if not self._files or vim.tbl_contains(self._files, filename) then
-      self._callback(self)
+  self.listener = function(filename)
+    if not self.files or vim.tbl_contains(self.files, filename) then
+      self.callback(self)
     end
   end
 
-  self._event:add(self._listener)
+  self.event:add(self.listener)
 end
 
 function Watcher:destroy()
-  log.line("watcher", "Watcher:destroy '%s'", self._path)
+  log.line("watcher", "Watcher:destroy '%s'", self.path)
 
-  self._event:remove(self._listener)
+  self.event:remove(self.listener)
 
-  utils.array_remove(Watcher._watchers, self)
+  utils.array_remove(watchers,       self)
 
   self.destroyed = true
 end
@@ -183,11 +210,11 @@ end
 function M.purge_watchers()
   log.line("watcher", "purge_watchers")
 
-  for _, w in ipairs(utils.array_shallow_clone(Watcher._watchers)) do
+  for _, w in ipairs(utils.array_shallow_clone(watchers)) do
     w:destroy()
   end
 
-  for _, e in pairs(Event._events) do
+  for _, e in pairs(events) do
     e:destroy()
   end
 end

From 3595418a8dbb9adc26fbb2f02140d27ebc811beb Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:09:32 +1100
Subject: [PATCH 40/88] chore: resolve undefined-field

---
 lua/nvim-tree/api.lua           | 7 +++++--
 lua/nvim-tree/explorer/init.lua | 2 +-
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua
index a2796cb10a2..bbb023a1aef 100644
--- a/lua/nvim-tree/api.lua
+++ b/lua/nvim-tree/api.lua
@@ -140,8 +140,11 @@ end)
 Api.tree.change_root_to_node = wrap_node(function(node)
   if node.name == ".." or node:is(RootNode) then
     actions.root.change_dir.fn("..")
-  elseif node:is(DirectoryNode) then
-    actions.root.change_dir.fn(node:last_group_node().absolute_path)
+  else
+    node = node:as(DirectoryNode)
+    if node then
+      actions.root.change_dir.fn(node:last_group_node().absolute_path)
+    end
   end
 end)
 
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 88e9048c5a3..9c7ae241229 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -98,7 +98,7 @@ function Explorer:create_autocmds()
     callback = function()
       appearance.setup()
       view.reset_winhl()
-      self:draw()
+      self.renderer:draw()
     end,
   })
 

From 558d90deb16873d3f3de1b86dce344e74140357e Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:10:56 +1100
Subject: [PATCH 41/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/tree/modifiers/expand-all.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
index 6032c686545..43ed5bafb04 100644
--- a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
@@ -60,7 +60,8 @@ local function gen_iterator()
         end
       end)
       :recursor(function(node)
-        return expansion_count < M.MAX_FOLDER_DISCOVERY and (node.group_next and { node.group_next } or (node.open and node.nodes))
+        node = node and node:as(DirectoryNode)
+        return expansion_count < M.MAX_FOLDER_DISCOVERY and node and (node.group_next and { node.group_next } or (node.open and node.nodes))
       end)
       :iterate()
 

From eacb61008dac0a79ccf981ba15c210ad6dfb6d3f Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:13:05 +1100
Subject: [PATCH 42/88] chore: resolve undefined-field

---
 lua/nvim-tree/watcher.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/watcher.lua b/lua/nvim-tree/watcher.lua
index 2b59a0cdf85..930a2dd3f21 100644
--- a/lua/nvim-tree/watcher.lua
+++ b/lua/nvim-tree/watcher.lua
@@ -192,7 +192,10 @@ function Watcher:destroy()
 
   self.event:remove(self.listener)
 
-  utils.array_remove(watchers,       self)
+  utils.array_remove(
+    watchers,
+    self
+  )
 
   self.destroyed = true
 end

From 5290897260cd3bc67de4b362c3af11a1621596b6 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:15:28 +1100
Subject: [PATCH 43/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/clipboard.lua | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua
index 93c16e87606..5cb88f15bf2 100644
--- a/lua/nvim-tree/actions/fs/clipboard.lua
+++ b/lua/nvim-tree/actions/fs/clipboard.lua
@@ -221,8 +221,11 @@ end
 function Clipboard:do_paste(node, action, action_fn)
   if node.name == ".." then
     node = self.explorer
-  elseif node:is(DirectoryNode) then
-    node = node:last_group_node()
+  else
+    local dir = node:as(DirectoryNode)
+    if dir then
+      node = dir:last_group_node()
+    end
   end
   local clip = self.data[action]
   if #clip == 0 then
@@ -373,7 +376,7 @@ function Clipboard:copy_path(node)
     end
 
     local relative_path = utils.path_relative(absolute_path, cwd)
-    content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path
+    content = node:is(DirectoryNode) and utils.path_add_trailing(relative_path) or relative_path
   end
 
   self:copy_to_reg(content)

From c883df94bf2abb983940e17455617a52f0258348 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:16:09 +1100
Subject: [PATCH 44/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/rename-file.lua | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lua/nvim-tree/actions/fs/rename-file.lua b/lua/nvim-tree/actions/fs/rename-file.lua
index da1ca001167..ad81849c5fd 100644
--- a/lua/nvim-tree/actions/fs/rename-file.lua
+++ b/lua/nvim-tree/actions/fs/rename-file.lua
@@ -125,8 +125,9 @@ function M.fn(default_modifier)
       return
     end
 
-    if node:is(DirectoryNode) then
-      node = node:last_group_node()
+    local dir = node:as(DirectoryNode)
+    if dir then
+      node = dir:last_group_node()
     end
     if node.name == ".." then
       return

From aa161a05999388fc3f5423943306cb0f9d8fa8c2 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:17:55 +1100
Subject: [PATCH 45/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/tree/modifiers/collapse-all.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
index 049be2bbfa2..760e1f7cd13 100644
--- a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
@@ -46,7 +46,8 @@ function M.fn(keep_buffers)
       end
     end)
     :recursor(function(n)
-      return n.group_next and { n.group_next } or n.nodes
+      n = n and n:as(DirectoryNode)
+      return n and (n.group_next and { n.group_next } or n.nodes)
     end)
     :iterate()
 

From 44281f79997b772439d5c86a525b1f2d231a355c Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:20:11 +1100
Subject: [PATCH 46/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/live-filter.lua | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/lua/nvim-tree/explorer/live-filter.lua b/lua/nvim-tree/explorer/live-filter.lua
index 9861195c4ba..c98f11dde55 100644
--- a/lua/nvim-tree/explorer/live-filter.lua
+++ b/lua/nvim-tree/explorer/live-filter.lua
@@ -37,18 +37,18 @@ local function reset_filter(self, node_)
   local dir_ = node_:as(DirectoryNode)
   if dir_ then
     dir_.hidden_stats = vim.tbl_deep_extend("force", dir_.hidden_stats or {}, { live_filter = 0, })
-  end
 
-  Iterator.builder(node_.nodes)
-    :hidden()
-    :applier(function(node)
-      node.hidden = false
-      local dir = node:as(DirectoryNode)
-      if dir then
-        dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
-      end
-    end)
-    :iterate()
+    Iterator.builder(dir_.nodes)
+      :hidden()
+      :applier(function(node)
+        node.hidden = false
+        local dir = node:as(DirectoryNode)
+        if dir then
+          dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
+        end
+      end)
+      :iterate()
+  end
 end
 
 local overlay_bufnr = 0

From 009b2c996e777e44866f6ad7730a45c8d8e718b0 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:35:18 +1100
Subject: [PATCH 47/88] chore: resolve undefined-field

---
 lua/nvim-tree/node/directory-link.lua | 2 +-
 lua/nvim-tree/node/directory.lua      | 4 ++--
 lua/nvim-tree/node/file-link.lua      | 2 +-
 lua/nvim-tree/node/file.lua           | 2 +-
 lua/nvim-tree/node/init.lua           | 4 ++--
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 210daddb516..ecf7e3f1319 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -4,7 +4,7 @@ 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
+---@field private fs_stat_target uv.fs_stat.result
 local DirectoryLinkNode = DirectoryNode:new()
 
 ---Static factory method
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index a04c017fa95..2fef08efba0 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -7,8 +7,8 @@ local Node = require("nvim-tree.node")
 ---@field group_next DirectoryNode? -- If node is grouped, this points to the next child dir/link node
 ---@field nodes Node[]
 ---@field open boolean
----@field watcher Watcher?
 ---@field hidden_stats table? -- Each field of this table is a key for source and value for count
+---@field private watcher Watcher?
 local DirectoryNode = Node:new()
 
 ---Static factory method
@@ -31,11 +31,11 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat)
     fs_stat = fs_stat,
     git_status = nil,
     hidden = false,
-    is_dot = false,
     name = name,
     parent = parent,
     watcher = nil,
     diag_status = nil,
+    is_dot = false,
 
     has_children = has_children,
     group_next = nil,
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index ecefb771576..5f9c6920967 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -4,7 +4,7 @@ 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
+---@field private fs_stat_target uv.fs_stat.result
 local FileLinkNode = FileNode:new()
 
 ---Static factory method
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index cbf83a6f6c3..52c5febcaea 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -24,10 +24,10 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat)
     fs_stat = fs_stat,
     git_status = nil,
     hidden = false,
-    is_dot = false,
     name = name,
     parent = parent,
     diag_status = nil,
+    is_dot = false,
 
     extension = string.match(name, ".?[^.]+%.(.*)") or "",
   }
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index 6d999b0fbf7..fe9984c46fe 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -13,7 +13,7 @@ local Class = require("nvim-tree.class")
 ---@field name string
 ---@field parent DirectoryNode?
 ---@field diag_status DiagStatus?
----@field is_dot boolean cached is_dotfile
+---@field private is_dot boolean cached is_dotfile
 local Node = Class:new()
 
 function Node:destroy()
@@ -82,10 +82,10 @@ function Node:clone()
     fs_stat = self.fs_stat,
     git_status = self.git_status,
     hidden = self.hidden,
-    is_dot = self.is_dot,
     name = self.name,
     parent = nil,
     diag_status = nil,
+    is_dot = self.is_dot,
   }
 
   return clone

From 622a8d1e3d1d7d75389293368edf4a04a9756faa Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 26 Oct 2024 15:48:57 +1100
Subject: [PATCH 48/88] chore: resolve undefined-field

---
 .../actions/tree/modifiers/collapse-all.lua   |  3 +--
 .../actions/tree/modifiers/expand-all.lua     |  3 +--
 lua/nvim-tree/explorer/live-filter.lua        | 22 +++++++++----------
 lua/nvim-tree/git/init.lua                    | 12 +++++-----
 4 files changed, 18 insertions(+), 22 deletions(-)

diff --git a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
index 760e1f7cd13..049be2bbfa2 100644
--- a/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua
@@ -46,8 +46,7 @@ function M.fn(keep_buffers)
       end
     end)
     :recursor(function(n)
-      n = n and n:as(DirectoryNode)
-      return n and (n.group_next and { n.group_next } or n.nodes)
+      return n.group_next and { n.group_next } or n.nodes
     end)
     :iterate()
 
diff --git a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
index 43ed5bafb04..6032c686545 100644
--- a/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
+++ b/lua/nvim-tree/actions/tree/modifiers/expand-all.lua
@@ -60,8 +60,7 @@ local function gen_iterator()
         end
       end)
       :recursor(function(node)
-        node = node and node:as(DirectoryNode)
-        return expansion_count < M.MAX_FOLDER_DISCOVERY and node and (node.group_next and { node.group_next } or (node.open and node.nodes))
+        return expansion_count < M.MAX_FOLDER_DISCOVERY and (node.group_next and { node.group_next } or (node.open and node.nodes))
       end)
       :iterate()
 
diff --git a/lua/nvim-tree/explorer/live-filter.lua b/lua/nvim-tree/explorer/live-filter.lua
index c98f11dde55..9861195c4ba 100644
--- a/lua/nvim-tree/explorer/live-filter.lua
+++ b/lua/nvim-tree/explorer/live-filter.lua
@@ -37,18 +37,18 @@ local function reset_filter(self, node_)
   local dir_ = node_:as(DirectoryNode)
   if dir_ then
     dir_.hidden_stats = vim.tbl_deep_extend("force", dir_.hidden_stats or {}, { live_filter = 0, })
-
-    Iterator.builder(dir_.nodes)
-      :hidden()
-      :applier(function(node)
-        node.hidden = false
-        local dir = node:as(DirectoryNode)
-        if dir then
-          dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
-        end
-      end)
-      :iterate()
   end
+
+  Iterator.builder(node_.nodes)
+    :hidden()
+    :applier(function(node)
+      node.hidden = false
+      local dir = node:as(DirectoryNode)
+      if dir then
+        dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
+      end
+    end)
+    :iterate()
 end
 
 local overlay_bufnr = 0
diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index e0333e456e5..8005a206ed6 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -204,28 +204,26 @@ local function reload_tree_at(toplevel)
   end
 
   log.line("watcher", "git event executing '%s'", toplevel)
-  local base = utils.get_node_from_path(toplevel)
-  base = base and base:as(DirectoryNode)
-  if not base then
+  local root_node = utils.get_node_from_path(toplevel)
+  if not root_node then
     return
   end
 
   M.reload_project(toplevel, nil, function()
     local git_status = M.get_project(toplevel)
 
-    Iterator.builder(base.nodes)
+    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)
       end)
       :recursor(function(node)
-        local dir = node:as(DirectoryNode)
-        return dir and #dir.nodes > 0 and dir.nodes
+        return node.nodes and #node.nodes > 0 and node.nodes
       end)
       :iterate()
 
-    base.explorer.renderer:draw()
+    root_node.explorer.renderer:draw()
   end)
 end
 

From bed7a072c3efd3b5f204bca56aa33df06ccf67f3 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 27 Oct 2024 09:51:29 +1100
Subject: [PATCH 49/88] chore: resolve undefined-field

---
 lua/nvim-tree/buffers.lua | 24 +++++++++++++++++++-----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/lua/nvim-tree/buffers.lua b/lua/nvim-tree/buffers.lua
index 7c03e406030..d53c1570df7 100644
--- a/lua/nvim-tree/buffers.lua
+++ b/lua/nvim-tree/buffers.lua
@@ -26,12 +26,26 @@ end
 ---@param node Node
 ---@return boolean
 function M.is_modified(node)
+  if not M.config.modified.enable then
+    return false
+  end
+
+  if not M._modified[node.absolute_path] then
+    return false
+  end
+
   local dir = node:as(DirectoryNode)
-  return node
-    and M.config.modified.enable
-    and M._modified[node.absolute_path]
-    and (not dir or M.config.modified.show_on_dirs)
-    and (not (dir and dir.open) or M.config.modified.show_on_open_dirs)
+  if dir then
+    if not M.config.modified.show_on_dirs then
+      return false
+    end
+
+    if dir.open and not M.config.modified.show_on_open_dirs then
+      return false
+    end
+  end
+
+  return true
 end
 
 ---A buffer exists for the node's absolute path

From 5c46c9c58a792c78ba1c71a2a3fcd8dd1b3b6c42 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 27 Oct 2024 13:42:39 +1100
Subject: [PATCH 50/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua  |  2 +-
 lua/nvim-tree/git/utils.lua | 45 ++++++++++++++++++++-----------------
 2 files changed, 25 insertions(+), 22 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 8005a206ed6..a97bd5df3d3 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -7,7 +7,7 @@ local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = require("nvim-tree.node.directory")
 
----@class GitStatus -- xy short-format statuses
+---@class (exact) GitStatus -- xy short-format statuses
 ---@field file string?
 ---@field dir table<"direct" | "indirect", string[]>?
 
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 75f1db5cb23..2faf0642fbe 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -129,44 +129,47 @@ end
 
 ---Git file status for an absolute path with optional fallback
 ---@param parent_ignored boolean
----@param status table|nil
+---@param status table?
 ---@param path string
 ---@param path_fallback string?
 ---@return GitStatus
 function M.git_status_file(parent_ignored, status, path, path_fallback)
-  if parent_ignored then
-    return { file = "!!" }
-  end
+  ---@type GitStatus
+  local st = {}
 
-  if not status or not status.files then
-    return {}
+  if parent_ignored then
+    st.file = "!!"
+  elseif status and status.files then
+    st.file = status.files[path] or status.files[path_fallback]
   end
 
-  return {
-    file = status.files[path] or status.files[path_fallback]
-  }
+  return st
 end
 
 ---Git file and directory status for an absolute path with optional file fallback
 ---@param parent_ignored boolean
----@param status table|nil
+---@param status table?
 ---@param path string
 ---@param path_file string? alternative file path when no other file status
----@return GitStatus|nil
+---@return GitStatus?
 function M.git_status_dir(parent_ignored, status, path, path_file)
+  ---@type GitStatus?
+  local st
+
   if parent_ignored then
-    return { file = "!!" }
+    st = {}
+    st.file = "!!"
+  elseif status then
+    st = {}
+    st.file = status.files and (status.files[path] or status.files[path_file])
+    if status.dirs then
+      st.dir = {}
+      st.dir.direct = status.dirs.direct and status.dirs.direct[path]
+      st.dir.indirect = status.dirs.indirect and status.dirs.indirect[path]
+    end
   end
 
-  if status then
-    return {
-      file = status.files and (status.files[path] or status.files[path_file]),
-      dir = status.dirs and {
-        direct = status.dirs.direct and status.dirs.direct[path],
-        indirect = status.dirs.indirect and status.dirs.indirect[path],
-      },
-    }
-  end
+  return st
 end
 
 function M.setup(opts)

From e82db1c44d7dbe444737470af68d44239d78d429 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 27 Oct 2024 14:37:59 +1100
Subject: [PATCH 51/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/utils.lua           | 45 ---------------------------
 lua/nvim-tree/node/directory-link.lua | 23 ++++++++++----
 lua/nvim-tree/node/directory.lua      | 17 ++++++++--
 lua/nvim-tree/node/file-link.lua      | 15 +++++----
 lua/nvim-tree/node/file.lua           |  8 +++--
 5 files changed, 46 insertions(+), 62 deletions(-)

diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 2faf0642fbe..8caaf73af6e 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -127,51 +127,6 @@ function M.file_status_to_dir_status(status, cwd)
   return r
 end
 
----Git file status for an absolute path with optional fallback
----@param parent_ignored boolean
----@param status table?
----@param path string
----@param path_fallback string?
----@return GitStatus
-function M.git_status_file(parent_ignored, status, path, path_fallback)
-  ---@type GitStatus
-  local st = {}
-
-  if parent_ignored then
-    st.file = "!!"
-  elseif status and status.files then
-    st.file = status.files[path] or status.files[path_fallback]
-  end
-
-  return st
-end
-
----Git file and directory status for an absolute path with optional file fallback
----@param parent_ignored boolean
----@param status table?
----@param path string
----@param path_file string? alternative file path when no other file status
----@return GitStatus?
-function M.git_status_dir(parent_ignored, status, path, path_file)
-  ---@type GitStatus?
-  local st
-
-  if parent_ignored then
-    st = {}
-    st.file = "!!"
-  elseif status then
-    st = {}
-    st.file = status.files and (status.files[path] or status.files[path_file])
-    if status.dirs then
-      st.dir = {}
-      st.dir.direct = status.dirs.direct and status.dirs.direct[path]
-      st.dir.indirect = status.dirs.indirect and status.dirs.indirect[path]
-    end
-  end
-
-  return st
-end
-
 function M.setup(opts)
   if opts.git.cygwin_support then
     M.use_cygpath = vim.fn.executable("cygpath") == 1
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index ecf7e3f1319..42faca64251 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -1,5 +1,3 @@
-local git_utils = require("nvim-tree.git.utils")
-
 local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class (exact) DirectoryLinkNode: DirectoryNode
@@ -36,11 +34,24 @@ function DirectoryLinkNode:destroy()
   DirectoryNode.destroy(self)
 end
 
------Update the directory GitStatus of link target and the file status of the link itself
------@param parent_ignored boolean
------@param status table|nil
+---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_utils.git_status_dir(parent_ignored, status, self.link_to, self.absolute_path)
+  if parent_ignored then
+    self.git_status = {}
+    self.git_status.file = "!!"
+  elseif status then
+    self.git_status = {}
+    self.git_status.file = status.files and (status.files[self.link_to] or status.files[self.absolute_path])
+    if status.dirs then
+      self.git_status.dir = {}
+      self.git_status.dir.direct = status.dirs.direct and status.dirs.direct[self.absolute_path]
+      self.git_status.dir.indirect = status.dirs.indirect and status.dirs.indirect[self.absolute_path]
+    end
+  else
+    self.git_status = nil
+  end
 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 2fef08efba0..9c019ec4a3f 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,5 +1,3 @@
-local git_utils = require("nvim-tree.git.utils")
-
 local Node = require("nvim-tree.node")
 
 ---@class (exact) DirectoryNode: Node
@@ -69,7 +67,20 @@ end
 ---@param parent_ignored boolean
 ---@param status table|nil
 function DirectoryNode:update_git_status(parent_ignored, status)
-  self.git_status = git_utils.git_status_dir(parent_ignored, status, self.absolute_path, nil)
+  if parent_ignored then
+    self.git_status = {}
+    self.git_status.file = "!!"
+  elseif status then
+    self.git_status = {}
+    self.git_status.file = status.files and status.files[self.absolute_path]
+    if status.dirs then
+      self.git_status.dir = {}
+      self.git_status.dir.direct = status.dirs.direct and status.dirs.direct[self.absolute_path]
+      self.git_status.dir.indirect = status.dirs.indirect and status.dirs.indirect[self.absolute_path]
+    end
+  else
+    self.git_status = nil
+  end
 end
 
 ---@return string[]? xy short-format statuses
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 5f9c6920967..e1fd9bfae58 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -1,5 +1,3 @@
-local git_utils = require("nvim-tree.git.utils")
-
 local FileNode = require("nvim-tree.node.file")
 
 ---@class (exact) FileLinkNode: FileNode
@@ -32,11 +30,16 @@ function FileLinkNode:destroy()
   FileNode.destroy(self)
 end
 
------Update the GitStatus of the target otherwise the link itself
------@param parent_ignored boolean
------@param status table|nil
+---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_utils.git_status_file(parent_ignored, status, self.link_to, self.absolute_path)
+  self.git_status = {}
+  if parent_ignored then
+    self.git_status.file = "!!"
+  elseif status and status.files then
+    self.git_status.file = status.files[self.link_to] or status.files[self.absolute_path]
+  end
 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 52c5febcaea..b411ddef134 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -1,4 +1,3 @@
-local git_utils = require("nvim-tree.git.utils")
 local utils = require("nvim-tree.utils")
 
 local Node = require("nvim-tree.node")
@@ -44,7 +43,12 @@ end
 ---@param parent_ignored boolean
 ---@param status table|nil
 function FileNode:update_git_status(parent_ignored, status)
-  self.git_status = git_utils.git_status_file(parent_ignored, status, self.absolute_path, nil)
+  self.git_status = {}
+  if parent_ignored then
+    self.git_status.file = "!!"
+  elseif status and status.files then
+    self.git_status.file = status.files[self.absolute_path]
+  end
 end
 
 ---@return string[]? xy short-format statuses

From a16e67f3f4cbe172ed8c8605b661b5e30267d35b Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 27 Oct 2024 15:18:05 +1100
Subject: [PATCH 52/88] Revert "chore: resolve undefined-field"

This reverts commit e82db1c44d7dbe444737470af68d44239d78d429.
---
 lua/nvim-tree/git/utils.lua           | 45 +++++++++++++++++++++++++++
 lua/nvim-tree/node/directory-link.lua | 23 ++++----------
 lua/nvim-tree/node/directory.lua      | 17 ++--------
 lua/nvim-tree/node/file-link.lua      | 15 ++++-----
 lua/nvim-tree/node/file.lua           |  8 ++---
 5 files changed, 62 insertions(+), 46 deletions(-)

diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 8caaf73af6e..2faf0642fbe 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -127,6 +127,51 @@ function M.file_status_to_dir_status(status, cwd)
   return r
 end
 
+---Git file status for an absolute path with optional fallback
+---@param parent_ignored boolean
+---@param status table?
+---@param path string
+---@param path_fallback string?
+---@return GitStatus
+function M.git_status_file(parent_ignored, status, path, path_fallback)
+  ---@type GitStatus
+  local st = {}
+
+  if parent_ignored then
+    st.file = "!!"
+  elseif status and status.files then
+    st.file = status.files[path] or status.files[path_fallback]
+  end
+
+  return st
+end
+
+---Git file and directory status for an absolute path with optional file fallback
+---@param parent_ignored boolean
+---@param status table?
+---@param path string
+---@param path_file string? alternative file path when no other file status
+---@return GitStatus?
+function M.git_status_dir(parent_ignored, status, path, path_file)
+  ---@type GitStatus?
+  local st
+
+  if parent_ignored then
+    st = {}
+    st.file = "!!"
+  elseif status then
+    st = {}
+    st.file = status.files and (status.files[path] or status.files[path_file])
+    if status.dirs then
+      st.dir = {}
+      st.dir.direct = status.dirs.direct and status.dirs.direct[path]
+      st.dir.indirect = status.dirs.indirect and status.dirs.indirect[path]
+    end
+  end
+
+  return st
+end
+
 function M.setup(opts)
   if opts.git.cygwin_support then
     M.use_cygpath = vim.fn.executable("cygpath") == 1
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 42faca64251..ecf7e3f1319 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -1,3 +1,5 @@
+local git_utils = require("nvim-tree.git.utils")
+
 local DirectoryNode = require("nvim-tree.node.directory")
 
 ---@class (exact) DirectoryLinkNode: DirectoryNode
@@ -34,24 +36,11 @@ function DirectoryLinkNode:destroy()
   DirectoryNode.destroy(self)
 end
 
----Update the directory GitStatus of link target and the file status of the link itself
----@param parent_ignored boolean
----@param status table|nil
+-----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)
-  if parent_ignored then
-    self.git_status = {}
-    self.git_status.file = "!!"
-  elseif status then
-    self.git_status = {}
-    self.git_status.file = status.files and (status.files[self.link_to] or status.files[self.absolute_path])
-    if status.dirs then
-      self.git_status.dir = {}
-      self.git_status.dir.direct = status.dirs.direct and status.dirs.direct[self.absolute_path]
-      self.git_status.dir.indirect = status.dirs.indirect and status.dirs.indirect[self.absolute_path]
-    end
-  else
-    self.git_status = nil
-  end
+  self.git_status = git_utils.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 9c019ec4a3f..2fef08efba0 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,3 +1,5 @@
+local git_utils = require("nvim-tree.git.utils")
+
 local Node = require("nvim-tree.node")
 
 ---@class (exact) DirectoryNode: Node
@@ -67,20 +69,7 @@ end
 ---@param parent_ignored boolean
 ---@param status table|nil
 function DirectoryNode:update_git_status(parent_ignored, status)
-  if parent_ignored then
-    self.git_status = {}
-    self.git_status.file = "!!"
-  elseif status then
-    self.git_status = {}
-    self.git_status.file = status.files and status.files[self.absolute_path]
-    if status.dirs then
-      self.git_status.dir = {}
-      self.git_status.dir.direct = status.dirs.direct and status.dirs.direct[self.absolute_path]
-      self.git_status.dir.indirect = status.dirs.indirect and status.dirs.indirect[self.absolute_path]
-    end
-  else
-    self.git_status = nil
-  end
+  self.git_status = git_utils.git_status_dir(parent_ignored, status, self.absolute_path, nil)
 end
 
 ---@return string[]? xy short-format statuses
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index e1fd9bfae58..5f9c6920967 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -1,3 +1,5 @@
+local git_utils = require("nvim-tree.git.utils")
+
 local FileNode = require("nvim-tree.node.file")
 
 ---@class (exact) FileLinkNode: FileNode
@@ -30,16 +32,11 @@ function FileLinkNode:destroy()
   FileNode.destroy(self)
 end
 
----Update the GitStatus of the target otherwise the link itself
----@param parent_ignored boolean
----@param status table|nil
+-----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 = {}
-  if parent_ignored then
-    self.git_status.file = "!!"
-  elseif status and status.files then
-    self.git_status.file = status.files[self.link_to] or status.files[self.absolute_path]
-  end
+  self.git_status = git_utils.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 b411ddef134..52c5febcaea 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -1,3 +1,4 @@
+local git_utils = require("nvim-tree.git.utils")
 local utils = require("nvim-tree.utils")
 
 local Node = require("nvim-tree.node")
@@ -43,12 +44,7 @@ end
 ---@param parent_ignored boolean
 ---@param status table|nil
 function FileNode:update_git_status(parent_ignored, status)
-  self.git_status = {}
-  if parent_ignored then
-    self.git_status.file = "!!"
-  elseif status and status.files then
-    self.git_status.file = status.files[self.absolute_path]
-  end
+  self.git_status = git_utils.git_status_file(parent_ignored, status, self.absolute_path, nil)
 end
 
 ---@return string[]? xy short-format statuses

From 2fba44f8c3ab8a140decd07c6e8970db8eb5d9f8 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 27 Oct 2024 17:22:04 +1100
Subject: [PATCH 53/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/filters.lua    |   2 +-
 lua/nvim-tree/explorer/init.lua       |  10 +--
 lua/nvim-tree/git/init.lua            | 109 ++++++++++++++++----------
 lua/nvim-tree/git/runner.lua          |  11 ++-
 lua/nvim-tree/git/utils.lua           |  31 ++++----
 lua/nvim-tree/node/directory-link.lua |   6 +-
 lua/nvim-tree/node/file-link.lua      |   6 +-
 7 files changed, 101 insertions(+), 74 deletions(-)

diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua
index 6cf21d4aad9..87f7f95ef8f 100644
--- a/lua/nvim-tree/explorer/filters.lua
+++ b/lua/nvim-tree/explorer/filters.lua
@@ -178,7 +178,7 @@ local function custom(self, path)
 end
 
 ---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons.
----@param git_status table|nil optional results of git.load_project_status(...)
+---@param git_status table|nil optional results of git.load_project(...)
 ---@return table
 --- git_status: reference
 --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 }
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 9c7ae241229..b787fffc2bc 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -321,7 +321,7 @@ function Explorer:refresh_parent_nodes_for_path(path)
     local project = git.get_project(toplevel) or {}
 
     self:reload(node, project)
-    git.update_parent_statuses(node, project, toplevel)
+    git.update_parent_projects(node, project, toplevel)
   end
 
   log.profile_end(profile)
@@ -331,7 +331,7 @@ end
 ---@param node DirectoryNode
 function Explorer:_load(node)
   local cwd = node.link_to or node.absolute_path
-  local git_status = git.load_project_status(cwd)
+  local git_status = git.load_project(cwd)
   self:explore(node, git_status, self)
 end
 
@@ -423,7 +423,7 @@ function Explorer:explore(node, status, parent)
   local single_child = node:single_child_directory()
   if config.renderer.group_empty and not is_root and single_child then
     local child_cwd = single_child.link_to or single_child.absolute_path
-    local child_status = git.load_project_status(child_cwd)
+    local child_status = git.load_project(child_cwd)
     node.group_next = single_child
     local ns = self:explore(single_child, child_status, parent)
     node.nodes = ns or {}
@@ -463,7 +463,7 @@ function Explorer:reload_explorer()
   end
   event_running = true
 
-  local projects = git.reload()
+  local projects = git.reload_all_projects()
   self:refresh_nodes(projects)
   if view.is_visible() then
     self.renderer:draw()
@@ -477,7 +477,7 @@ function Explorer:reload_git()
   end
   event_running = true
 
-  local projects = git.reload()
+  local projects = git.reload_all_projects()
   git.reload_node_status(self, projects)
   self.renderer:draw()
   event_running = false
diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index a97bd5df3d3..da3736093ba 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -7,20 +7,39 @@ local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = require("nvim-tree.node.directory")
 
----@class (exact) GitStatus -- xy short-format statuses
+---Git xy short-format statuses for a single node
+---@class (exact) GitStatus
 ---@field file string?
 ---@field dir table<"direct" | "indirect", string[]>?
 
+-- Git xy short-format status
+---@alias GitPathXY table<string, string>
+
+-- Git xy short-format statuses
+---@alias GitPathXYs table<string, string[]>
+
+---@alias GitProjectFiles GitPathXY
+---@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs>
+
+---Git state for an entire repo
+---@class (exact) GitProject
+---@field files GitProjectFiles?
+---@field dirs GitProjectDirs?
+---@field watcher Watcher?
+
 local M = {
   config = {},
 
-  -- all projects keyed by toplevel
+  ---all projects keyed by toplevel
+  ---@type table<string, GitProject>
   _projects_by_toplevel = {},
 
-  -- index of paths inside toplevels, false when not inside a project
+  ---index of paths inside toplevels, false when not inside a project
+  ---@type table<string, string|false>
   _toplevels_by_path = {},
 
   -- git dirs by toplevel
+  ---@type table<string, string>
   _git_dirs_by_toplevel = {},
 }
 
@@ -36,33 +55,33 @@ local WATCHED_FILES = {
 
 ---@param toplevel string|nil
 ---@param path string|nil
----@param project table
----@param statuses GitXYByPath?
-local function reload_git_statuses(toplevel, path, project, statuses)
+---@param project GitProject
+---@param project_files GitProjectFiles?
+local function reload_git_project(toplevel, path, project, project_files)
   if path then
     for p in pairs(project.files) do
       if p:find(path, 1, true) == 1 then
         project.files[p] = nil
       end
     end
-    project.files = vim.tbl_deep_extend("force", project.files, statuses)
+    project.files = vim.tbl_deep_extend("force", project.files, project_files)
   else
-    project.files = statuses
+    project.files = project_files or {}
   end
 
-  project.dirs = git_utils.file_status_to_dir_status(project.files, toplevel)
+  project.dirs = git_utils.project_files_to_project_dirs(project.files, toplevel)
 end
 
 --- Is this path in a known ignored directory?
 ---@param path string
----@param project table git status
+---@param project GitProject
 ---@return boolean
 local function path_ignored_in_project(path, project)
   if not path or not project then
     return false
   end
 
-  if project and project.files then
+  if project.files then
     for file, status in pairs(project.files) do
       if status == "!!" and vim.startswith(path, file) then
         return true
@@ -72,9 +91,8 @@ local function path_ignored_in_project(path, project)
   return false
 end
 
---- Reload all projects
----@return table projects maybe empty
-function M.reload()
+---@return GitProject[] maybe empty
+function M.reload_all_projects()
   if not M.config.git.enable then
     return {}
   end
@@ -87,11 +105,12 @@ function M.reload()
 end
 
 --- Reload one project. Does nothing when no project or path is ignored
----@param toplevel string|nil
----@param path string|nil optional path to update only
----@param callback function|nil
+---@param toplevel string?
+---@param path string? optional path to update only
+---@param callback function?
 function M.reload_project(toplevel, path, callback)
-  local project = M._projects_by_toplevel[toplevel]
+  local project = M._projects_by_toplevel[toplevel] --[[@as GitProject]]
+
   if not toplevel or not project or not M.config.git.enable then
     if callback then
       callback()
@@ -116,21 +135,21 @@ function M.reload_project(toplevel, path, callback)
   }
 
   if callback then
-    ---@param statuses GitXYByPath
+    ---@param statuses GitPathXY
     runner_opts.callback = function(statuses)
-      reload_git_statuses(toplevel, path, project, statuses)
+      reload_git_project(toplevel, path, project, statuses)
       callback()
     end
     GitRunner:run(runner_opts)
   else
     -- TODO #1974 use callback once async/await is available
-    reload_git_statuses(toplevel, path, project, GitRunner:run(runner_opts))
+    reload_git_project(toplevel, path, project, GitRunner:run(runner_opts))
   end
 end
 
 --- Retrieve a known project
----@param toplevel string|nil
----@return table|nil project
+---@param toplevel string?
+---@return GitProject? project
 function M.get_project(toplevel)
   return M._projects_by_toplevel[toplevel]
 end
@@ -151,11 +170,10 @@ function M.get_toplevel(path)
     return nil
   end
 
-  if M._toplevels_by_path[path] then
-    return M._toplevels_by_path[path]
-  end
-
-  if M._toplevels_by_path[path] == false then
+  local tl = M._toplevels_by_path[path]
+  if tl then
+    return tl
+  elseif tl == false then
     return nil
   end
 
@@ -194,8 +212,15 @@ function M.get_toplevel(path)
   end
 
   M._toplevels_by_path[path] = toplevel
+
   M._git_dirs_by_toplevel[toplevel] = git_dir
-  return M._toplevels_by_path[path]
+
+  toplevel = M._toplevels_by_path[path]
+  if toplevel == false then
+    return nil
+  else
+    return toplevel
+  end
 end
 
 local function reload_tree_at(toplevel)
@@ -230,8 +255,8 @@ end
 --- Load the project status for a path. Does nothing when no toplevel for path.
 --- Only fetches project status when unknown, otherwise returns existing.
 ---@param path string absolute
----@return table project maybe empty
-function M.load_project_status(path)
+---@return GitProject maybe empty
+function M.load_project(path)
   if not M.config.git.enable then
     return {}
   end
@@ -242,12 +267,12 @@ function M.load_project_status(path)
     return {}
   end
 
-  local status = M._projects_by_toplevel[toplevel]
-  if status then
-    return status
+  local project = M._projects_by_toplevel[toplevel]
+  if project then
+    return project
   end
 
-  local statuses = GitRunner:run({
+  local path_xys = GitRunner:run({
     toplevel = toplevel,
     list_untracked = git_utils.should_show_untracked(toplevel),
     list_ignored = true,
@@ -275,10 +300,10 @@ function M.load_project_status(path)
     })
   end
 
-  if statuses then
+  if path_xys then
     M._projects_by_toplevel[toplevel] = {
-      files = statuses,
-      dirs = git_utils.file_status_to_dir_status(statuses, toplevel),
+      files = path_xys,
+      dirs = git_utils.project_files_to_project_dirs(path_xys, toplevel),
       watcher = watcher,
     }
     return M._projects_by_toplevel[toplevel]
@@ -289,9 +314,9 @@ function M.load_project_status(path)
 end
 
 ---@param dir DirectoryNode
----@param project table?
+---@param project GitProject?
 ---@param root string?
-function M.update_parent_statuses(dir, project, root)
+function M.update_parent_projects(dir, project, root)
   while project and dir do
     -- step up to the containing project
     if dir.absolute_path == root then
@@ -331,14 +356,14 @@ function M.refresh_dir(dir)
 
     dir.explorer:reload(node, project)
 
-    M.update_parent_statuses(dir, project, toplevel)
+    M.update_parent_projects(dir, project, toplevel)
 
     dir.explorer.renderer:draw()
   end)
 end
 
 ---@param dir DirectoryNode?
----@param projects table
+---@param projects GitProject[]
 function M.reload_node_status(dir, projects)
   dir = dir and dir:as(DirectoryNode)
   if not dir or #dir.nodes == 0 then
diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua
index d4163905549..d8e8f64c960 100644
--- a/lua/nvim-tree/git/runner.lua
+++ b/lua/nvim-tree/git/runner.lua
@@ -4,19 +4,17 @@ local notify = require("nvim-tree.notify")
 
 local Class = require("nvim-tree.class")
 
----@alias GitXYByPath table<string, string> -- short-format statuses
-
 ---@class (exact) GitRunnerOpts
 ---@field toplevel string absolute path
 ---@field path string? absolute path
 ---@field list_untracked boolean
 ---@field list_ignored boolean
 ---@field timeout integer
----@field callback fun(statuses: GitXYByPath)?
+---@field callback fun(statuses: GitPathXY)?
 
 ---@class (exact) GitRunner: Class
 ---@field private opts GitRunnerOpts
----@field private statuses GitXYByPath
+---@field private statuses GitPathXY
 ---@field private rc integer? -- -1 indicates timeout
 local GitRunner = Class:new()
 
@@ -207,7 +205,8 @@ function GitRunner:finalise()
   end
 end
 
----@return GitXYByPath? statuses nil if callback present
+---@private
+---@return GitPathXY? statuses nil if callback present
 function GitRunner:execute()
   local async = self.opts.callback ~= nil
   local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.opts.toplevel, self.opts.path)
@@ -240,7 +239,7 @@ end
 
 ---Static method to run a git process, which will be killed if it takes more than timeout
 ---@param opts GitRunnerOpts
----@return GitXYByPath? statuses nil if callback present
+---@return GitPathXY? statuses nil if callback present
 function GitRunner:run(opts)
   ---@type GitRunner
   local runner = {
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 2faf0642fbe..d1cb056b6d6 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -82,8 +82,8 @@ function M.should_show_untracked(cwd)
   return untracked[cwd]
 end
 
----@param t table|nil
----@param k string
+---@param t table<string|integer, boolean>?
+---@param k string|integer
 ---@return table
 local function nil_insert(t, k)
   t = t or {}
@@ -91,31 +91,33 @@ local function nil_insert(t, k)
   return t
 end
 
----@param status table
+---@param project_files GitProjectFiles
 ---@param cwd string|nil
----@return table
-function M.file_status_to_dir_status(status, cwd)
-  local direct = {}
-  for p, s in pairs(status) do
+---@return GitProjectDirs
+function M.project_files_to_project_dirs(project_files, cwd)
+  ---@type GitProjectDirs
+  local project_dirs = {}
+
+  project_dirs.direct = {}
+  for p, s in pairs(project_files) do
     if s ~= "!!" then
       local modified = vim.fn.fnamemodify(p, ":h")
-      direct[modified] = nil_insert(direct[modified], s)
+      project_dirs.direct[modified] = nil_insert(project_dirs.direct[modified], s)
     end
   end
 
-  local indirect = {}
-  for dirname, statuses in pairs(direct) do
+  project_dirs.indirect = {}
+  for dirname, statuses in pairs(project_dirs.direct) do
     for s, _ in pairs(statuses) do
       local modified = dirname
       while modified ~= cwd and modified ~= "/" do
         modified = vim.fn.fnamemodify(modified, ":h")
-        indirect[modified] = nil_insert(indirect[modified], s)
+        project_dirs.indirect[modified] = nil_insert(project_dirs.indirect[modified], s)
       end
     end
   end
 
-  local r = { indirect = indirect, direct = direct }
-  for _, d in pairs(r) do
+  for _, d in pairs(project_dirs) do
     for dirname, statuses in pairs(d) do
       local new_statuses = {}
       for s, _ in pairs(statuses) do
@@ -124,7 +126,8 @@ function M.file_status_to_dir_status(status, cwd)
       d[dirname] = new_statuses
     end
   end
-  return r
+
+  return project_dirs
 end
 
 ---Git file status for an absolute path with optional fallback
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index ecf7e3f1319..b5dfb47e422 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -36,9 +36,9 @@ function DirectoryLinkNode:destroy()
   DirectoryNode.destroy(self)
 end
 
------Update the directory GitStatus of link target and the file status of the link itself
------@param parent_ignored boolean
------@param status table|nil
+---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_utils.git_status_dir(parent_ignored, status, self.link_to, self.absolute_path)
 end
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 5f9c6920967..ca7cb3a9bf0 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -32,9 +32,9 @@ function FileLinkNode:destroy()
   FileNode.destroy(self)
 end
 
------Update the GitStatus of the target otherwise the link itself
------@param parent_ignored boolean
------@param status table|nil
+---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_utils.git_status_file(parent_ignored, status, self.link_to, self.absolute_path)
 end

From 9b365279df5fa0f4f6d89c8a45fca43ad52c804b Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 14:00:52 +1100
Subject: [PATCH 54/88] chore: class new is now generic

---
 lua/nvim-tree/actions/fs/create-file.lua         |  2 +-
 lua/nvim-tree/class.lua                          | 14 ++++++++------
 lua/nvim-tree/explorer/init.lua                  |  2 +-
 lua/nvim-tree/git/runner.lua                     |  2 +-
 lua/nvim-tree/node/directory-link.lua            |  2 +-
 lua/nvim-tree/node/directory.lua                 |  2 +-
 lua/nvim-tree/node/file-link.lua                 |  2 +-
 lua/nvim-tree/node/file.lua                      |  2 +-
 lua/nvim-tree/node/root.lua                      |  2 +-
 lua/nvim-tree/renderer/decorator/bookmarks.lua   |  2 +-
 lua/nvim-tree/renderer/decorator/copied.lua      |  2 +-
 lua/nvim-tree/renderer/decorator/cut.lua         |  2 +-
 lua/nvim-tree/renderer/decorator/diagnostics.lua |  2 +-
 lua/nvim-tree/renderer/decorator/git.lua         |  2 +-
 lua/nvim-tree/renderer/decorator/hidden.lua      |  2 +-
 lua/nvim-tree/renderer/decorator/modified.lua    |  2 +-
 lua/nvim-tree/renderer/decorator/opened.lua      |  2 +-
 lua/nvim-tree/watcher.lua                        |  4 ++--
 18 files changed, 26 insertions(+), 24 deletions(-)

diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua
index 455f6f564f7..86145cdb6d5 100644
--- a/lua/nvim-tree/actions/fs/create-file.lua
+++ b/lua/nvim-tree/actions/fs/create-file.lua
@@ -34,7 +34,7 @@ end
 
 ---@param node Node?
 function M.fn(node)
-  node = node or core.get_explorer() --[[@as Node]]
+  node = node or core.get_explorer()
   if not node then
     return
   end
diff --git a/lua/nvim-tree/class.lua b/lua/nvim-tree/class.lua
index a43cbd8af54..33b0b5bd33d 100644
--- a/lua/nvim-tree/class.lua
+++ b/lua/nvim-tree/class.lua
@@ -1,22 +1,24 @@
 ---Generic class, useful for inheritence.
 ---@class (exact) Class
----@field private __index? table
 local Class = {}
 
----@param o Class?
----@return Class
+---@generic T
+---@param self T
+---@param o T|nil
+---@return T
 function Class:new(o)
   o = o or {}
 
   setmetatable(o, self)
-  self.__index = self
+  self.__index = self ---@diagnostic disable-line: inject-field
 
   return o
 end
 
 ---Object is an instance of class
 ---This will start with the lowest class and loop over all the superclasses.
----@param class table
+---@generic T
+---@param class T
 ---@return boolean
 function Class:is(class)
   local mt = getmetatable(self)
@@ -32,7 +34,7 @@ end
 ---Return object if it is an instance of class, otherwise nil
 ---@generic T
 ---@param class T
----@return `T`|nil
+---@return T|nil
 function Class:as(class)
   return self:is(class) and self or nil
 end
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index b787fffc2bc..4e4b25e7cca 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -59,7 +59,7 @@ function Explorer:create(path)
 
   local o = RootNode:create(explorer_placeholder, path, "..", nil)
 
-  o = self:new(o) --[[@as Explorer]]
+  o = self:new(o)
 
   o.explorer = o
 
diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua
index d8e8f64c960..a450ea519c3 100644
--- a/lua/nvim-tree/git/runner.lua
+++ b/lua/nvim-tree/git/runner.lua
@@ -246,7 +246,7 @@ function GitRunner:run(opts)
     opts = opts,
     statuses = {},
   }
-  runner = GitRunner:new(runner) --[[@as GitRunner]]
+  runner = GitRunner:new(runner)
 
   return runner:execute()
 end
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index b5dfb47e422..4b917ca28d5 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -20,7 +20,7 @@ function DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name
   -- 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]]
+  o = self:new(o)
 
   -- reset absolute path to the link itself
   o.absolute_path = absolute_path
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 2fef08efba0..f492129eb1d 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -43,7 +43,7 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat)
     open = false,
     hidden_stats = nil,
   }
-  o = self:new(o) --[[@as DirectoryNode]]
+  o = self:new(o)
 
   o.watcher = require("nvim-tree.explorer.watch").create_watcher(o)
 
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index ca7cb3a9bf0..eebf3864e25 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -19,7 +19,7 @@ local FileLinkNode = FileNode:new()
 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 = self:new(o)
 
   o.type = "link"
   o.link_to = link_to
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 52c5febcaea..98cfaa55272 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -31,7 +31,7 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat)
 
     extension = string.match(name, ".?[^.]+%.(.*)") or "",
   }
-  o = self:new(o) --[[@as FileNode]]
+  o = self:new(o)
 
   return o
 end
diff --git a/lua/nvim-tree/node/root.lua b/lua/nvim-tree/node/root.lua
index 2fd037cecf5..0544a141a12 100644
--- a/lua/nvim-tree/node/root.lua
+++ b/lua/nvim-tree/node/root.lua
@@ -12,7 +12,7 @@ local RootNode = DirectoryNode:new()
 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]]
+  o = self:new(o)
 
   return o
 end
diff --git a/lua/nvim-tree/renderer/decorator/bookmarks.lua b/lua/nvim-tree/renderer/decorator/bookmarks.lua
index 6b33970fe90..3c188721c0c 100644
--- a/lua/nvim-tree/renderer/decorator/bookmarks.lua
+++ b/lua/nvim-tree/renderer/decorator/bookmarks.lua
@@ -19,7 +19,7 @@ function DecoratorBookmarks:create(opts, explorer)
     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]]
+  o = self:new(o)
 
   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 0debcc632bb..3d760a97a55 100644
--- a/lua/nvim-tree/renderer/decorator/copied.lua
+++ b/lua/nvim-tree/renderer/decorator/copied.lua
@@ -19,7 +19,7 @@ function DecoratorCopied:create(opts, explorer)
     hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none,
     icon_placement = ICON_PLACEMENT.none,
   }
-  o = self:new(o) --[[@as DecoratorCopied]]
+  o = self:new(o)
 
   return o
 end
diff --git a/lua/nvim-tree/renderer/decorator/cut.lua b/lua/nvim-tree/renderer/decorator/cut.lua
index b81642f6e79..45428969e7e 100644
--- a/lua/nvim-tree/renderer/decorator/cut.lua
+++ b/lua/nvim-tree/renderer/decorator/cut.lua
@@ -18,7 +18,7 @@ function DecoratorCut:create(opts, explorer)
     hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none,
     icon_placement = ICON_PLACEMENT.none,
   }
-  o = self:new(o) --[[@as DecoratorCut]]
+  o = self:new(o)
 
   return o
 end
diff --git a/lua/nvim-tree/renderer/decorator/diagnostics.lua b/lua/nvim-tree/renderer/decorator/diagnostics.lua
index ef59ff70e03..ee495ca674e 100644
--- a/lua/nvim-tree/renderer/decorator/diagnostics.lua
+++ b/lua/nvim-tree/renderer/decorator/diagnostics.lua
@@ -49,7 +49,7 @@ function DecoratorDiagnostics:create(opts, explorer)
     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]]
+  o = self:new(o)
 
   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 67ba1ad1773..628b2d2e6c6 100644
--- a/lua/nvim-tree/renderer/decorator/git.lua
+++ b/lua/nvim-tree/renderer/decorator/git.lua
@@ -34,7 +34,7 @@ function DecoratorGit:create(opts, explorer)
     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]]
+  o = self:new(o)
 
   if not o.enabled then
     return o
diff --git a/lua/nvim-tree/renderer/decorator/hidden.lua b/lua/nvim-tree/renderer/decorator/hidden.lua
index 000d84461c1..7c62f51af5f 100644
--- a/lua/nvim-tree/renderer/decorator/hidden.lua
+++ b/lua/nvim-tree/renderer/decorator/hidden.lua
@@ -20,7 +20,7 @@ function DecoratorHidden:create(opts, explorer)
     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]]
+  o = self:new(o)
 
   if opts.renderer.icons.show.hidden then
     o.icon = {
diff --git a/lua/nvim-tree/renderer/decorator/modified.lua b/lua/nvim-tree/renderer/decorator/modified.lua
index 0cd2d55c191..2126379cd14 100644
--- a/lua/nvim-tree/renderer/decorator/modified.lua
+++ b/lua/nvim-tree/renderer/decorator/modified.lua
@@ -22,7 +22,7 @@ function DecoratorModified:create(opts, explorer)
     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]]
+  o = self:new(o)
 
   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 6f2ad58bbfc..cb1843311c7 100644
--- a/lua/nvim-tree/renderer/decorator/opened.lua
+++ b/lua/nvim-tree/renderer/decorator/opened.lua
@@ -21,7 +21,7 @@ function DecoratorOpened:create(opts, explorer)
     hl_pos = HL_POSITION[opts.renderer.highlight_opened_files] or HL_POSITION.none,
     icon_placement = ICON_PLACEMENT.none,
   }
-  o = self:new(o) --[[@as DecoratorOpened]]
+  o = self:new(o)
 
   return o
 end
diff --git a/lua/nvim-tree/watcher.lua b/lua/nvim-tree/watcher.lua
index 930a2dd3f21..0dfdbf0afde 100644
--- a/lua/nvim-tree/watcher.lua
+++ b/lua/nvim-tree/watcher.lua
@@ -40,7 +40,7 @@ function Event:create(path)
     fs_event = nil,
     listeners = {},
   }
-  o = self:new(o) --[[@as Event]]
+  o = self:new(o)
 
   if o:start() then
     events[path] = o
@@ -168,7 +168,7 @@ function Watcher:create(path, files, callback, data)
     listener = nil,
     event = event,
   }
-  o = self:new(o) --[[@as Watcher]]
+  o = self:new(o)
 
   o:start()
 

From 4372e6a217a5785fac08f0aef0cc0226c1921246 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 15:09:27 +1100
Subject: [PATCH 55/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua   |  8 +++----
 lua/nvim-tree/git/runner.lua | 14 ++++++------
 lua/nvim-tree/git/utils.lua  | 44 ++++++++++++++++++------------------
 lua/nvim-tree/node/init.lua  |  2 +-
 4 files changed, 34 insertions(+), 34 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index da3736093ba..4c28e7c6de5 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -8,7 +8,7 @@ local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = require("nvim-tree.node.directory")
 
 ---Git xy short-format statuses for a single node
----@class (exact) GitStatus
+---@class (exact) GitNodeStatus
 ---@field file string?
 ---@field dir table<"direct" | "indirect", string[]>?
 
@@ -135,9 +135,9 @@ function M.reload_project(toplevel, path, callback)
   }
 
   if callback then
-    ---@param statuses GitPathXY
-    runner_opts.callback = function(statuses)
-      reload_git_project(toplevel, path, project, statuses)
+    ---@param path_xy GitPathXY
+    runner_opts.callback = function(path_xy)
+      reload_git_project(toplevel, path, project, path_xy)
       callback()
     end
     GitRunner:run(runner_opts)
diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua
index a450ea519c3..9cb13d1c9e4 100644
--- a/lua/nvim-tree/git/runner.lua
+++ b/lua/nvim-tree/git/runner.lua
@@ -10,11 +10,11 @@ local Class = require("nvim-tree.class")
 ---@field list_untracked boolean
 ---@field list_ignored boolean
 ---@field timeout integer
----@field callback fun(statuses: GitPathXY)?
+---@field callback fun(path_xy: GitPathXY)?
 
 ---@class (exact) GitRunner: Class
 ---@field private opts GitRunnerOpts
----@field private statuses GitPathXY
+---@field private path_xy GitPathXY
 ---@field private rc integer? -- -1 indicates timeout
 local GitRunner = Class:new()
 
@@ -34,7 +34,7 @@ function GitRunner:parse_status_output(status, path)
     path = path:gsub("/", "\\")
   end
   if #status > 0 and #path > 0 then
-    self.statuses[utils.path_remove_trailing(utils.path_join({ self.opts.toplevel, path }))] = status
+    self.path_xy[utils.path_remove_trailing(utils.path_join({ self.opts.toplevel, path }))] = status
   end
 end
 
@@ -218,7 +218,7 @@ function GitRunner:execute()
 
       self:finalise()
 
-      self.opts.callback(self.statuses)
+      self.opts.callback(self.path_xy)
     end)
   else
     -- sync, maybe call back
@@ -230,9 +230,9 @@ function GitRunner:execute()
     self:finalise()
 
     if self.opts.callback then
-      self.opts.callback(self.statuses)
+      self.opts.callback(self.path_xy)
     else
-      return self.statuses
+      return self.path_xy
     end
   end
 end
@@ -244,7 +244,7 @@ function GitRunner:run(opts)
   ---@type GitRunner
   local runner = {
     opts = opts,
-    statuses = {},
+    path_xy = {},
   }
   runner = GitRunner:new(runner)
 
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index d1cb056b6d6..32a678cf1a6 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -130,49 +130,49 @@ function M.project_files_to_project_dirs(project_files, cwd)
   return project_dirs
 end
 
----Git file status for an absolute path with optional fallback
+---Git file status for an absolute path
 ---@param parent_ignored boolean
 ---@param status table?
 ---@param path string
----@param path_fallback string?
----@return GitStatus
+---@param path_fallback string? alternative file path when no other file status
+---@return GitNodeStatus
 function M.git_status_file(parent_ignored, status, path, path_fallback)
-  ---@type GitStatus
-  local st = {}
+  ---@type GitNodeStatus
+  local ns = {}
 
   if parent_ignored then
-    st.file = "!!"
+    ns.file = "!!"
   elseif status and status.files then
-    st.file = status.files[path] or status.files[path_fallback]
+    ns.file = status.files[path] or status.files[path_fallback]
   end
 
-  return st
+  return ns
 end
 
----Git file and directory status for an absolute path with optional file fallback
+---Git file and directory status for an absolute path
 ---@param parent_ignored boolean
 ---@param status table?
 ---@param path string
----@param path_file string? alternative file path when no other file status
----@return GitStatus?
-function M.git_status_dir(parent_ignored, status, path, path_file)
-  ---@type GitStatus?
-  local st
+---@param path_fallback string? alternative file path when no other file status
+---@return GitNodeStatus?
+function M.git_status_dir(parent_ignored, status, path, path_fallback)
+  ---@type GitNodeStatus?
+  local ns
 
   if parent_ignored then
-    st = {}
-    st.file = "!!"
+    ns = {}
+    ns.file = "!!"
   elseif status then
-    st = {}
-    st.file = status.files and (status.files[path] or status.files[path_file])
+    ns = {}
+    ns.file = status.files and (status.files[path] or status.files[path_fallback])
     if status.dirs then
-      st.dir = {}
-      st.dir.direct = status.dirs.direct and status.dirs.direct[path]
-      st.dir.indirect = status.dirs.indirect and status.dirs.indirect[path]
+      ns.dir = {}
+      ns.dir.direct = status.dirs.direct and status.dirs.direct[path]
+      ns.dir.indirect = status.dirs.indirect and status.dirs.indirect[path]
     end
   end
 
-  return st
+  return ns
 end
 
 function M.setup(opts)
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index fe9984c46fe..4b191a40988 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -8,7 +8,7 @@ local Class = require("nvim-tree.class")
 ---@field absolute_path string
 ---@field executable boolean
 ---@field fs_stat uv.fs_stat.result?
----@field git_status GitStatus?
+---@field git_status GitNodeStatus?
 ---@field hidden boolean
 ---@field name string
 ---@field parent DirectoryNode?

From a4a6e6caa324fe8b550cb30a43da065a723ef11b Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 15:15:23 +1100
Subject: [PATCH 56/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua   | 12 ++++++------
 lua/nvim-tree/git/runner.lua |  6 ++++--
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 4c28e7c6de5..74d159b4501 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -82,8 +82,8 @@ local function path_ignored_in_project(path, project)
   end
 
   if project.files then
-    for file, status in pairs(project.files) do
-      if status == "!!" and vim.startswith(path, file) then
+    for p, xy in pairs(project.files) do
+      if xy == "!!" and vim.startswith(path, p) then
         return true
       end
     end
@@ -235,13 +235,13 @@ local function reload_tree_at(toplevel)
   end
 
   M.reload_project(toplevel, nil, function()
-    local git_status = M.get_project(toplevel)
+    local project = M.get_project(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)
+        node:update_git_status(parent_ignored, project)
       end)
       :recursor(function(node)
         return node.nodes and #node.nodes > 0 and node.nodes
@@ -371,9 +371,9 @@ function M.reload_node_status(dir, projects)
   end
 
   local toplevel = M.get_toplevel(dir.absolute_path)
-  local status = projects[toplevel] or {}
+  local project = projects[toplevel] or {}
   for _, node in ipairs(dir.nodes) do
-    node:update_git_status(dir:is_git_ignored(), status)
+    node:update_git_status(dir:is_git_ignored(), project)
     M.reload_node_status(node:as(DirectoryNode), projects)
   end
 end
diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua
index 9cb13d1c9e4..13b57383cf1 100644
--- a/lua/nvim-tree/git/runner.lua
+++ b/lua/nvim-tree/git/runner.lua
@@ -205,8 +205,9 @@ function GitRunner:finalise()
   end
 end
 
+---Return nil when callback present
 ---@private
----@return GitPathXY? statuses nil if callback present
+---@return GitPathXY?
 function GitRunner:execute()
   local async = self.opts.callback ~= nil
   local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.opts.toplevel, self.opts.path)
@@ -238,8 +239,9 @@ function GitRunner:execute()
 end
 
 ---Static method to run a git process, which will be killed if it takes more than timeout
+---Return nil when callback present
 ---@param opts GitRunnerOpts
----@return GitPathXY? statuses nil if callback present
+---@return GitPathXY?
 function GitRunner:run(opts)
   ---@type GitRunner
   local runner = {

From 142cb30b3d54c5a6ca94d58e9fc0120ea89ba023 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 15:37:19 +1100
Subject: [PATCH 57/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/moves/item.lua     |  6 +++---
 lua/nvim-tree/git/init.lua               | 23 ++++++++++++---------
 lua/nvim-tree/node/directory-link.lua    |  2 +-
 lua/nvim-tree/node/directory.lua         | 22 ++++++++++----------
 lua/nvim-tree/node/file-link.lua         |  2 +-
 lua/nvim-tree/node/file.lua              |  4 ++--
 lua/nvim-tree/node/init.lua              |  7 ++++---
 lua/nvim-tree/renderer/decorator/git.lua | 26 ++++++++++++------------
 8 files changed, 48 insertions(+), 44 deletions(-)

diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index edd4b5c385b..f6555f573e4 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -11,14 +11,14 @@ local MAX_DEPTH = 100
 
 ---Return the status of the node or nil if no status, depending on the type of
 ---status.
----@param node table node to inspect
+---@param node Node to inspect
 ---@param what string type of status
 ---@param skip_gitignored boolean default false
 ---@return boolean
 local function status_is_valid(node, what, skip_gitignored)
   if what == "git" then
-    local git_status = node:get_git_status()
-    return git_status ~= nil and (not skip_gitignored or git_status[1] ~= "!!")
+    local git_xy = node:get_git_xy()
+    return git_xy ~= nil and (not skip_gitignored or git_xy[1] ~= "!!")
   elseif what == "diag" then
     local diag_status = diagnostics.get_diag_status(node)
     return diag_status ~= nil and diag_status.value ~= nil
diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 74d159b4501..548d5a4b15f 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -7,19 +7,19 @@ local Watcher = require("nvim-tree.watcher").Watcher
 local Iterator = require("nvim-tree.iterators.node-iterator")
 local DirectoryNode = require("nvim-tree.node.directory")
 
----Git xy short-format statuses for a single node
----@class (exact) GitNodeStatus
----@field file string?
----@field dir table<"direct" | "indirect", string[]>?
+---Git short format status xy
+---@alias GitXY string
 
--- Git xy short-format status
----@alias GitPathXY table<string, string>
+-- Git short-format status
+---@alias GitPathXY table<string, GitXY>
 
--- Git xy short-format statuses
----@alias GitPathXYs table<string, string[]>
+-- Git short-format statuses
+---@alias GitPathXYs table<string, GitXY[]>
 
----@alias GitProjectFiles GitPathXY
----@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs>
+---Git short-format statuses for a single node
+---@class (exact) GitNodeStatus
+---@field file GitXY
+---@field dir table<"direct" | "indirect", GitXY[]>?
 
 ---Git state for an entire repo
 ---@class (exact) GitProject
@@ -27,6 +27,9 @@ local DirectoryNode = require("nvim-tree.node.directory")
 ---@field dirs GitProjectDirs?
 ---@field watcher Watcher?
 
+---@alias GitProjectFiles GitPathXY
+---@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs>
+
 local M = {
   config = {},
 
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 4b917ca28d5..15212e499ae 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -36,7 +36,7 @@ function DirectoryLinkNode:destroy()
   DirectoryNode.destroy(self)
 end
 
----Update the directory GitStatus of link target and the file status of the link itself
+---Update the directory git_status 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)
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index f492129eb1d..fb3c5287769 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -65,41 +65,41 @@ function DirectoryNode:destroy()
   Node.destroy(self)
 end
 
----Update the GitStatus of the directory
+---Update the git_status of the directory
 ---@param parent_ignored boolean
 ---@param status table|nil
 function DirectoryNode:update_git_status(parent_ignored, status)
   self.git_status = git_utils.git_status_dir(parent_ignored, status, self.absolute_path, nil)
 end
 
----@return string[]? xy short-format statuses
-function DirectoryNode:get_git_status()
+---@return GitXY[]?
+function DirectoryNode:get_git_xy()
   if not self.git_status or not self.explorer.opts.git.show_on_dirs then
     return nil
   end
 
-  local status = {}
+  local xys = {}
   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)
+      table.insert(xys, 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)
+          table.insert(xys, 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)
+          table.insert(xys, 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)
+      table.insert(xys, self.git_status.file)
     end
     if self.git_status.dir ~= nil and self.git_status.dir.direct ~= nil then
       local deleted = {
@@ -110,15 +110,15 @@ function DirectoryNode:get_git_status()
       }
       for _, s in pairs(self.git_status.dir.direct) do
         if deleted[s] then
-          table.insert(status, s)
+          table.insert(xys, s)
         end
       end
     end
   end
-  if #status == 0 then
+  if #xys == 0 then
     return nil
   else
-    return status
+    return xys
   end
 end
 
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index eebf3864e25..089b2a2508a 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -32,7 +32,7 @@ function FileLinkNode:destroy()
   FileNode.destroy(self)
 end
 
----Update the GitStatus of the target otherwise the link itself
+---Update the git_status of the target otherwise the link itself
 ---@param parent_ignored boolean
 ---@param status table|nil
 function FileLinkNode:update_git_status(parent_ignored, status)
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 98cfaa55272..22463f2665f 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -47,8 +47,8 @@ function FileNode:update_git_status(parent_ignored, status)
   self.git_status = git_utils.git_status_file(parent_ignored, status, self.absolute_path, nil)
 end
 
----@return string[]? xy short-format statuses
-function FileNode:get_git_status()
+---@return GitXY[]?
+function FileNode:get_git_xy()
   if not self.git_status then
     return nil
   end
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index 4b191a40988..57e6f3ca23c 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -19,7 +19,7 @@ local Node = Class:new()
 function Node:destroy()
 end
 
----Update the GitStatus of the node
+---Update the git_status of the node
 ---Abstract
 ---@param parent_ignored boolean
 ---@param status table?
@@ -27,8 +27,9 @@ function Node:update_git_status(parent_ignored, status)
   self:nop(parent_ignored, status)
 end
 
----@return string[]? xy short-format statuses
-function Node:get_git_status()
+---Short-format statuses
+---@return GitXY[]?
+function Node:get_git_xy()
 end
 
 ---@return boolean
diff --git a/lua/nvim-tree/renderer/decorator/git.lua b/lua/nvim-tree/renderer/decorator/git.lua
index 628b2d2e6c6..4a543bcd30a 100644
--- a/lua/nvim-tree/renderer/decorator/git.lua
+++ b/lua/nvim-tree/renderer/decorator/git.lua
@@ -12,12 +12,12 @@ local DirectoryNode = require("nvim-tree.node.directory")
 ---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked"
 
 ---@alias GitIconsByStatus table<GitStatusStrings, GitHighlightedString> human status
----@alias GitIconsByXY table<string, GitHighlightedString[]> porcelain status
----@alias GitGlyphs table<GitStatusStrings, string> from opts
+---@alias GitIconsByXY table<GitXY, GitHighlightedString[]> porcelain status
+---@alias GitGlyphsByStatus table<GitStatusStrings, string> from opts
 
 ---@class (exact) DecoratorGit: Decorator
----@field file_hl_by_xy table<string, string>?
----@field folder_hl_by_xy table<string, string>?
+---@field file_hl_by_xy table<GitXY, string>?
+---@field folder_hl_by_xy table<GitXY, string>?
 ---@field icons_by_status GitIconsByStatus?
 ---@field icons_by_xy GitIconsByXY?
 local DecoratorGit = Decorator:new()
@@ -56,7 +56,7 @@ function DecoratorGit:create(opts, explorer)
   return o
 end
 
----@param glyphs GitGlyphs
+---@param glyphs GitGlyphsByStatus
 function DecoratorGit:build_icons_by_status(glyphs)
   self.icons_by_status = {}
   self.icons_by_status.staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }
@@ -154,19 +154,19 @@ function DecoratorGit:calculate_icons(node)
     return nil
   end
 
-  local git_status = node:get_git_status()
-  if git_status == nil then
+  local git_xy = node:get_git_xy()
+  if git_xy == nil then
     return nil
   end
 
   local inserted = {}
   local iconss = {}
 
-  for _, s in pairs(git_status) do
+  for _, s in pairs(git_xy) do
     local icons = self.icons_by_xy[s]
     if not icons then
       if self.hl_pos == HL_POSITION.none then
-        notify.warn(string.format("Unrecognized git state '%s'", git_status))
+        notify.warn(string.format("Unrecognized git state '%s'", git_xy))
       end
       return nil
     end
@@ -215,15 +215,15 @@ function DecoratorGit:calculate_highlight(node)
     return nil
   end
 
-  local git_status = node:get_git_status()
-  if not git_status then
+  local git_xy = node:get_git_xy()
+  if not git_xy then
     return nil
   end
 
   if node:is(DirectoryNode) then
-    return self.folder_hl_by_xy[git_status[1]]
+    return self.folder_hl_by_xy[git_xy[1]]
   else
-    return self.file_hl_by_xy[git_status[1]]
+    return self.file_hl_by_xy[git_xy[1]]
   end
 end
 

From 4aba93b25e4d5cfd76dfe6cdd92b98b2e03fd87a Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 16:01:02 +1100
Subject: [PATCH 58/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/filters.lua    |  6 ++--
 lua/nvim-tree/explorer/init.lua       | 40 +++++++++++++--------------
 lua/nvim-tree/node/directory-link.lua |  6 ++--
 lua/nvim-tree/node/directory.lua      |  6 ++--
 lua/nvim-tree/node/file-link.lua      |  6 ++--
 lua/nvim-tree/node/file.lua           |  6 ++--
 lua/nvim-tree/node/init.lua           |  6 ++--
 7 files changed, 38 insertions(+), 38 deletions(-)

diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua
index 87f7f95ef8f..08f071ce55b 100644
--- a/lua/nvim-tree/explorer/filters.lua
+++ b/lua/nvim-tree/explorer/filters.lua
@@ -178,14 +178,14 @@ local function custom(self, path)
 end
 
 ---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons.
----@param git_status table|nil optional results of git.load_project(...)
+---@param project GitProject? optional results of git.load_projects(...)
 ---@return table
 --- git_status: reference
 --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 }
 --- bookmarks: absolute paths to boolean
-function Filters:prepare(git_status)
+function Filters:prepare(project)
   local status = {
-    git_status = git_status or {},
+    git_status = project or {},
     bufinfo = {},
     bookmarks = {},
   }
diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 4e4b25e7cca..8d2b304e9fa 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -187,9 +187,9 @@ function Explorer:expand(node)
 end
 
 ---@param node DirectoryNode
----@param git_status table|nil
+---@param project GitProject?
 ---@return Node[]?
-function Explorer:reload(node, git_status)
+function Explorer:reload(node, project)
   local cwd = node.link_to or node.absolute_path
   local handle = vim.loop.fs_scandir(cwd)
   if not handle then
@@ -198,7 +198,7 @@ function Explorer:reload(node, git_status)
 
   local profile = log.profile_start("reload %s", node.absolute_path)
 
-  local filter_status = self.filters:prepare(git_status)
+  local filter_status = self.filters:prepare(project)
 
   if node.group_next then
     node.nodes = { node.group_next }
@@ -268,7 +268,7 @@ function Explorer:reload(node, git_status)
   end
 
   node.nodes = vim.tbl_map(
-    self:update_status(nodes_by_path, node_ignored, git_status),
+    self:update_git_statuses(nodes_by_path, node_ignored, project),
     vim.tbl_filter(function(n)
       if remain_childs[n.absolute_path] then
         return remain_childs[n.absolute_path]
@@ -282,7 +282,7 @@ function Explorer:reload(node, git_status)
   local single_child = node:single_child_directory()
   if config.renderer.group_empty and node.parent and single_child then
     node.group_next = single_child
-    local ns = self:reload(single_child, git_status)
+    local ns = self:reload(single_child, project)
     node.nodes = ns or {}
     log.profile_end(profile)
     return ns
@@ -331,19 +331,19 @@ end
 ---@param node DirectoryNode
 function Explorer:_load(node)
   local cwd = node.link_to or node.absolute_path
-  local git_status = git.load_project(cwd)
-  self:explore(node, git_status, self)
+  local project = git.load_project(cwd)
+  self:explore(node, project, self)
 end
 
 ---@private
 ---@param nodes_by_path Node[]
 ---@param node_ignored boolean
----@param status table|nil
+---@param project GitProject?
 ---@return fun(node: Node): table
-function Explorer:update_status(nodes_by_path, node_ignored, status)
+function Explorer:update_git_statuses(nodes_by_path, node_ignored, project)
   return function(node)
     if nodes_by_path[node.absolute_path] then
-      node:update_git_status(node_ignored, status)
+      node:update_git_status(node_ignored, project)
     end
     return node
   end
@@ -353,13 +353,13 @@ end
 ---@param handle uv.uv_fs_t
 ---@param cwd string
 ---@param node DirectoryNode
----@param git_status table
+---@param project GitProject
 ---@param parent Explorer
-function Explorer:populate_children(handle, cwd, node, git_status, parent)
+function Explorer:populate_children(handle, cwd, node, project, parent)
   local node_ignored = node:is_git_ignored()
   local nodes_by_path = utils.bool_record(node.nodes, "absolute_path")
 
-  local filter_status = parent.filters:prepare(git_status)
+  local filter_status = parent.filters:prepare(project)
 
   node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
     git = 0,
@@ -388,7 +388,7 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent)
         if child then
           table.insert(node.nodes, child)
           nodes_by_path[child.absolute_path] = true
-          child:update_git_status(node_ignored, git_status)
+          child:update_git_status(node_ignored, project)
         end
       else
         for reason, value in pairs(FILTER_REASON) do
@@ -405,10 +405,10 @@ end
 
 ---@private
 ---@param node DirectoryNode
----@param status table
+---@param project GitProject
 ---@param parent Explorer
 ---@return Node[]|nil
-function Explorer:explore(node, status, parent)
+function Explorer:explore(node, project, parent)
   local cwd = node.link_to or node.absolute_path
   local handle = vim.loop.fs_scandir(cwd)
   if not handle then
@@ -417,15 +417,15 @@ function Explorer:explore(node, status, parent)
 
   local profile = log.profile_start("explore %s", node.absolute_path)
 
-  self:populate_children(handle, cwd, node, status, parent)
+  self:populate_children(handle, cwd, node, project, parent)
 
   local is_root = not node.parent
   local single_child = node:single_child_directory()
   if config.renderer.group_empty and not is_root and single_child then
     local child_cwd = single_child.link_to or single_child.absolute_path
-    local child_status = git.load_project(child_cwd)
+    local child_project = git.load_project(child_cwd)
     node.group_next = single_child
-    local ns = self:explore(single_child, child_status, parent)
+    local ns = self:explore(single_child, child_project, parent)
     node.nodes = ns or {}
 
     log.profile_end(profile)
@@ -440,7 +440,7 @@ function Explorer:explore(node, status, parent)
 end
 
 ---@private
----@param projects table
+---@param projects GitProject[]
 function Explorer:refresh_nodes(projects)
   Iterator.builder({ self })
     :applier(function(n)
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 15212e499ae..8e19a9ee98d 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -38,9 +38,9 @@ end
 
 ---Update the directory git_status 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_utils.git_status_dir(parent_ignored, status, self.link_to, self.absolute_path)
+---@param project GitProject?
+function DirectoryLinkNode:update_git_status(parent_ignored, project)
+  self.git_status = git_utils.git_status_dir(parent_ignored, project, 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 fb3c5287769..0af31389f27 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -67,9 +67,9 @@ end
 
 ---Update the git_status of the directory
 ---@param parent_ignored boolean
----@param status table|nil
-function DirectoryNode:update_git_status(parent_ignored, status)
-  self.git_status = git_utils.git_status_dir(parent_ignored, status, self.absolute_path, nil)
+---@param project GitProject?
+function DirectoryNode:update_git_status(parent_ignored, project)
+  self.git_status = git_utils.git_status_dir(parent_ignored, project, self.absolute_path, nil)
 end
 
 ---@return GitXY[]?
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 089b2a2508a..1faa2f04208 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -34,9 +34,9 @@ end
 
 ---Update the git_status 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_utils.git_status_file(parent_ignored, status, self.link_to, self.absolute_path)
+---@param project GitProject?
+function FileLinkNode:update_git_status(parent_ignored, project)
+  self.git_status = git_utils.git_status_file(parent_ignored, project, 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 22463f2665f..4f0216fe6d0 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -42,9 +42,9 @@ end
 
 ---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_utils.git_status_file(parent_ignored, status, self.absolute_path, nil)
+---@param project GitProject?
+function FileNode:update_git_status(parent_ignored, project)
+  self.git_status = git_utils.git_status_file(parent_ignored, project, self.absolute_path, nil)
 end
 
 ---@return GitXY[]?
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index 57e6f3ca23c..af6770c3592 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -22,9 +22,9 @@ end
 ---Update the git_status of the node
 ---Abstract
 ---@param parent_ignored boolean
----@param status table?
-function Node:update_git_status(parent_ignored, status)
-  self:nop(parent_ignored, status)
+---@param project GitProject?
+function Node:update_git_status(parent_ignored, project)
+  self:nop(parent_ignored, project)
 end
 
 ---Short-format statuses

From 1c8b343e84cb095c36c830b7fcc605733c21c503 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 16:20:54 +1100
Subject: [PATCH 59/88] chore: resolve undefined-field

---
 lua/nvim-tree/git/init.lua  |  2 +-
 lua/nvim-tree/git/utils.lua | 31 +++++++++++++++++++------------
 2 files changed, 20 insertions(+), 13 deletions(-)

diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua
index 548d5a4b15f..10354f85f30 100644
--- a/lua/nvim-tree/git/init.lua
+++ b/lua/nvim-tree/git/init.lua
@@ -18,7 +18,7 @@ local DirectoryNode = require("nvim-tree.node.directory")
 
 ---Git short-format statuses for a single node
 ---@class (exact) GitNodeStatus
----@field file GitXY
+---@field file GitXY?
 ---@field dir table<"direct" | "indirect", GitXY[]>?
 
 ---Git state for an entire repo
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index 32a678cf1a6..c8580ce8c97 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -138,12 +138,18 @@ end
 ---@return GitNodeStatus
 function M.git_status_file(parent_ignored, status, path, path_fallback)
   ---@type GitNodeStatus
-  local ns = {}
+  local ns
 
   if parent_ignored then
-    ns.file = "!!"
+    ns = {
+      file = "!!"
+    }
   elseif status and status.files then
-    ns.file = status.files[path] or status.files[path_fallback]
+    ns = {
+      file = status.files[path] or status.files[path_fallback]
+    }
+  else
+    ns = {}
   end
 
   return ns
@@ -160,16 +166,17 @@ function M.git_status_dir(parent_ignored, status, path, path_fallback)
   local ns
 
   if parent_ignored then
-    ns = {}
-    ns.file = "!!"
+    ns = {
+      file = "!!"
+    }
   elseif status then
-    ns = {}
-    ns.file = status.files and (status.files[path] or status.files[path_fallback])
-    if status.dirs then
-      ns.dir = {}
-      ns.dir.direct = status.dirs.direct and status.dirs.direct[path]
-      ns.dir.indirect = status.dirs.indirect and status.dirs.indirect[path]
-    end
+    ns = {
+      file = status.files and (status.files[path] or status.files[path_fallback]),
+      dir = status.dirs and {
+        direct = status.dirs.direct and status.dirs.direct[path],
+        indirect = status.dirs.indirect and status.dirs.indirect[path],
+      },
+    }
   end
 
   return ns

From c8b6848672ca1a2bb19cc484dc521948173625ab Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 16:30:16 +1100
Subject: [PATCH 60/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/filters.lua | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua
index 08f071ce55b..8de9f15076b 100644
--- a/lua/nvim-tree/explorer/filters.lua
+++ b/lua/nvim-tree/explorer/filters.lua
@@ -57,25 +57,25 @@ end
 
 ---Check if the given path is git clean/ignored
 ---@param path string Absolute path
----@param git_status table from prepare
+---@param project GitProject from prepare
 ---@return boolean
-local function git(self, path, git_status)
-  if type(git_status) ~= "table" or type(git_status.files) ~= "table" or type(git_status.dirs) ~= "table" then
+local function git(self, path, project)
+  if type(project) ~= "table" or type(project.files) ~= "table" or type(project.dirs) ~= "table" then
     return false
   end
 
   -- default status to clean
-  local status = git_status.files[path]
-  status = status or git_status.dirs.direct[path] and git_status.dirs.direct[path][1]
-  status = status or git_status.dirs.indirect[path] and git_status.dirs.indirect[path][1]
+  local xy = project.files[path]
+  xy = xy or project.dirs.direct[path] and project.dirs.direct[path][1]
+  xy = xy or project.dirs.indirect[path] and project.dirs.indirect[path][1]
 
   -- filter ignored; overrides clean as they are effectively dirty
-  if self.config.filter_git_ignored and status == "!!" then
+  if self.config.filter_git_ignored and xy == "!!" then
     return true
   end
 
   -- filter clean
-  if self.config.filter_git_clean and not status then
+  if self.config.filter_git_clean and not xy then
     return true
   end
 
@@ -180,12 +180,12 @@ end
 ---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons.
 ---@param project GitProject? optional results of git.load_projects(...)
 ---@return table
---- git_status: reference
+--- project: reference
 --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 }
 --- bookmarks: absolute paths to boolean
 function Filters:prepare(project)
   local status = {
-    git_status = project or {},
+    project = project or {},
     bufinfo = {},
     bookmarks = {},
   }
@@ -219,7 +219,7 @@ function Filters:should_filter(path, fs_stat, status)
     return false
   end
 
-  return git(self, path, status.git_status)
+  return git(self, path, status.project)
     or buf(self, path, status.bufinfo)
     or dotfile(self, path)
     or custom(self, path)
@@ -240,7 +240,7 @@ function Filters:should_filter_as_reason(path, fs_stat, status)
     return FILTER_REASON.none
   end
 
-  if git(self, path, status.git_status) then
+  if git(self, path, status.project) then
     return FILTER_REASON.git
   elseif buf(self, path, status.bufinfo) then
     return FILTER_REASON.buf

From caed01019c17254917e04acdb90e7eee98ad90df Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 16:36:10 +1100
Subject: [PATCH 61/88] chore: resolve undefined-field

---
 lua/nvim-tree/explorer/init.lua |  2 +-
 lua/nvim-tree/git/utils.lua     | 22 +++++++++++-----------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 8d2b304e9fa..7aa612341cf 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -339,7 +339,7 @@ end
 ---@param nodes_by_path Node[]
 ---@param node_ignored boolean
 ---@param project GitProject?
----@return fun(node: Node): table
+---@return fun(node: Node): Node
 function Explorer:update_git_statuses(nodes_by_path, node_ignored, project)
   return function(node)
     if nodes_by_path[node.absolute_path] then
diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua
index c8580ce8c97..a5a1efc21b1 100644
--- a/lua/nvim-tree/git/utils.lua
+++ b/lua/nvim-tree/git/utils.lua
@@ -132,11 +132,11 @@ end
 
 ---Git file status for an absolute path
 ---@param parent_ignored boolean
----@param status table?
+---@param project GitProject?
 ---@param path string
 ---@param path_fallback string? alternative file path when no other file status
 ---@return GitNodeStatus
-function M.git_status_file(parent_ignored, status, path, path_fallback)
+function M.git_status_file(parent_ignored, project, path, path_fallback)
   ---@type GitNodeStatus
   local ns
 
@@ -144,9 +144,9 @@ function M.git_status_file(parent_ignored, status, path, path_fallback)
     ns = {
       file = "!!"
     }
-  elseif status and status.files then
+  elseif project and project.files then
     ns = {
-      file = status.files[path] or status.files[path_fallback]
+      file = project.files[path] or project.files[path_fallback]
     }
   else
     ns = {}
@@ -157,11 +157,11 @@ end
 
 ---Git file and directory status for an absolute path
 ---@param parent_ignored boolean
----@param status table?
+---@param project GitProject?
 ---@param path string
 ---@param path_fallback string? alternative file path when no other file status
 ---@return GitNodeStatus?
-function M.git_status_dir(parent_ignored, status, path, path_fallback)
+function M.git_status_dir(parent_ignored, project, path, path_fallback)
   ---@type GitNodeStatus?
   local ns
 
@@ -169,12 +169,12 @@ function M.git_status_dir(parent_ignored, status, path, path_fallback)
     ns = {
       file = "!!"
     }
-  elseif status then
+  elseif project then
     ns = {
-      file = status.files and (status.files[path] or status.files[path_fallback]),
-      dir = status.dirs and {
-        direct = status.dirs.direct and status.dirs.direct[path],
-        indirect = status.dirs.indirect and status.dirs.indirect[path],
+      file = project.files and (project.files[path] or project.files[path_fallback]),
+      dir = project.dirs and {
+        direct = project.dirs.direct and project.dirs.direct[path],
+        indirect = project.dirs.indirect and project.dirs.indirect[path],
       },
     }
   end

From a789224e7122363ebba8c4f052ddfd7d3fd8b531 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 16:54:50 +1100
Subject: [PATCH 62/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/moves/item.lua | 35 +++++++++++++---------------
 1 file changed, 16 insertions(+), 19 deletions(-)

diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua
index f6555f573e4..8aacaae3b56 100644
--- a/lua/nvim-tree/actions/moves/item.lua
+++ b/lua/nvim-tree/actions/moves/item.lua
@@ -12,8 +12,8 @@ local MAX_DEPTH = 100
 ---Return the status of the node or nil if no status, depending on the type of
 ---status.
 ---@param node Node to inspect
----@param what string type of status
----@param skip_gitignored boolean default false
+---@param what string? type of status
+---@param skip_gitignored boolean? default false
 ---@return boolean
 local function status_is_valid(node, what, skip_gitignored)
   if what == "git" then
@@ -31,9 +31,9 @@ end
 
 ---Move to the next node that has a valid status. If none found, don't move.
 ---@param explorer Explorer
----@param where string where to move (forwards or backwards)
----@param what string type of status
----@param skip_gitignored boolean default false
+---@param where string? where to move (forwards or backwards)
+---@param what string? type of status
+---@param skip_gitignored boolean? default false
 local function move(explorer, where, what, skip_gitignored)
   local first_node_line = core.get_nodes_starting_line()
   local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line)
@@ -84,8 +84,8 @@ end
 
 --- Move to the next node recursively.
 ---@param explorer Explorer
----@param what string type of status
----@param skip_gitignored boolean default false
+---@param what string? type of status
+---@param skip_gitignored? boolean default false
 local function move_next_recursive(explorer, what, skip_gitignored)
   -- If the current node:
   -- * is a directory
@@ -150,8 +150,8 @@ end
 --- 4.5) Save the current node and start back from 4.1.
 ---
 ---@param explorer Explorer
----@param what string type of status
----@param skip_gitignored boolean default false
+---@param what string? type of status
+---@param skip_gitignored boolean? default false
 local function move_prev_recursive(explorer, what, skip_gitignored)
   local node_init, node_cur
 
@@ -210,8 +210,10 @@ local function move_prev_recursive(explorer, what, skip_gitignored)
 end
 
 ---@class NavigationItemOpts
----@field where string
----@field what string
+---@field where string?
+---@field what string?
+---@field skip_gitignored boolean?
+---@field recurse boolean?
 
 ---@param opts NavigationItemOpts
 ---@return fun()
@@ -223,26 +225,21 @@ function M.fn(opts)
     end
 
     local recurse = false
-    local skip_gitignored = false
 
     -- recurse only valid for git and diag moves.
     if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then
       recurse = opts.recurse
     end
 
-    if opts.skip_gitignored ~= nil then
-      skip_gitignored = opts.skip_gitignored
-    end
-
     if not recurse then
-      move(explorer, opts.where, opts.what, skip_gitignored)
+      move(explorer, opts.where, opts.what, opts.skip_gitignored)
       return
     end
 
     if opts.where == "next" then
-      move_next_recursive(explorer, opts.what, skip_gitignored)
+      move_next_recursive(explorer, opts.what, opts.skip_gitignored)
     elseif opts.where == "prev" then
-      move_prev_recursive(explorer, opts.what, skip_gitignored)
+      move_prev_recursive(explorer, opts.what, opts.skip_gitignored)
     end
   end
 end

From e5ac77bce660d9348e553b73ebd05df952cfeebf Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 17:19:44 +1100
Subject: [PATCH 63/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/clipboard.lua | 52 +++++++++++++-------------
 1 file changed, 25 insertions(+), 27 deletions(-)

diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua
index 5cb88f15bf2..78e8d5451c0 100644
--- a/lua/nvim-tree/actions/fs/clipboard.lua
+++ b/lua/nvim-tree/actions/fs/clipboard.lua
@@ -9,27 +9,25 @@ local find_file = require("nvim-tree.actions.finders.find-file").fn
 
 local DirectoryNode = require("nvim-tree.node.directory")
 
----@enum ACTION
-local ACTION = {
-  copy = "copy",
-  cut = "cut",
-}
+---@alias ClipboardAction "copy" | "cut"
+---@alias ClipboardData table<ClipboardAction, Node[]>
 
 ---@class Clipboard to handle all actions.fs clipboard API
 ---@field config table hydrated user opts.filters
 ---@field private explorer Explorer
----@field private data table<ACTION, Node[]>
+---@field private data ClipboardData
 local Clipboard = {}
 
 ---@param opts table user options
 ---@param explorer Explorer
 ---@return Clipboard
 function Clipboard:new(opts, explorer)
+  ---@type Clipboard
   local o = {
     explorer = explorer,
     data = {
-      [ACTION.copy] = {},
-      [ACTION.cut] = {},
+      copy = {},
+      cut = {},
     },
     config = {
       filesystem_watchers = opts.filesystem_watchers,
@@ -109,7 +107,7 @@ end
 
 ---@param source string
 ---@param dest string
----@param action ACTION
+---@param action ClipboardAction
 ---@param action_fn fun(source: string, dest: string)
 ---@return boolean|nil -- success
 ---@return string|nil -- error message
@@ -173,7 +171,7 @@ local function do_single_paste(source, dest, action, action_fn)
 end
 
 ---@param node Node
----@param clip table
+---@param clip ClipboardData
 local function toggle(node, clip)
   if node.name == ".." then
     return
@@ -191,8 +189,8 @@ end
 
 ---Clear copied and cut
 function Clipboard:clear_clipboard()
-  self.data[ACTION.copy] = {}
-  self.data[ACTION.cut] = {}
+  self.data.copy = {}
+  self.data.cut = {}
   notify.info("Clipboard has been emptied.")
   self.explorer.renderer:draw()
 end
@@ -200,23 +198,23 @@ end
 ---Copy one node
 ---@param node Node
 function Clipboard:copy(node)
-  utils.array_remove(self.data[ACTION.cut], node)
-  toggle(node, self.data[ACTION.copy])
+  utils.array_remove(self.data.cut, node)
+  toggle(node, self.data.copy)
   self.explorer.renderer:draw()
 end
 
 ---Cut one node
 ---@param node Node
 function Clipboard:cut(node)
-  utils.array_remove(self.data[ACTION.copy], node)
-  toggle(node, self.data[ACTION.cut])
+  utils.array_remove(self.data.copy, node)
+  toggle(node, self.data.cut)
   self.explorer.renderer:draw()
 end
 
 ---Paste cut or cop
 ---@private
 ---@param node Node
----@param action ACTION
+---@param action ClipboardAction
 ---@param action_fn fun(source: string, dest: string)
 function Clipboard:do_paste(node, action, action_fn)
   if node.name == ".." then
@@ -281,24 +279,24 @@ end
 ---Paste cut (if present) or copy (if present)
 ---@param node Node
 function Clipboard:paste(node)
-  if self.data[ACTION.cut][1] ~= nil then
-    self:do_paste(node, ACTION.cut, do_cut)
-  elseif self.data[ACTION.copy][1] ~= nil then
-    self:do_paste(node, ACTION.copy, do_copy)
+  if self.data.cut[1] ~= nil then
+    self:do_paste(node, "cut", do_cut)
+  elseif self.data.copy[1] ~= nil then
+    self:do_paste(node, "copy", do_copy)
   end
 end
 
 function Clipboard:print_clipboard()
   local content = {}
-  if #self.data[ACTION.cut] > 0 then
+  if #self.data.cut > 0 then
     table.insert(content, "Cut")
-    for _, node in pairs(self.data[ACTION.cut]) do
+    for _, node in pairs(self.data.cut) do
       table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
     end
   end
-  if #self.data[ACTION.copy] > 0 then
+  if #self.data.copy > 0 then
     table.insert(content, "Copy")
-    for _, node in pairs(self.data[ACTION.copy]) do
+    for _, node in pairs(self.data.copy) do
       table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
     end
   end
@@ -397,14 +395,14 @@ end
 ---@param node Node
 ---@return boolean
 function Clipboard:is_cut(node)
-  return vim.tbl_contains(self.data[ACTION.cut], node)
+  return vim.tbl_contains(self.data.cut, node)
 end
 
 ---Node is copied. Will not be cut.
 ---@param node Node
 ---@return boolean
 function Clipboard:is_copied(node)
-  return vim.tbl_contains(self.data[ACTION.copy], node)
+  return vim.tbl_contains(self.data.copy, node)
 end
 
 return Clipboard

From f6e238b4bc4a6afea97f8e49e8c51610102033fc Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 17:27:11 +1100
Subject: [PATCH 64/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/clipboard.lua | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua
index 78e8d5451c0..23266d424c4 100644
--- a/lua/nvim-tree/actions/fs/clipboard.lua
+++ b/lua/nvim-tree/actions/fs/clipboard.lua
@@ -45,10 +45,8 @@ end
 ---@return boolean
 ---@return string|nil
 local function do_copy(source, destination)
-  local source_stats, handle
-  local success, errmsg
+  local source_stats, errmsg = vim.loop.fs_stat(source)
 
-  source_stats, errmsg = vim.loop.fs_stat(source)
   if not source_stats then
     log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg)
     return false, errmsg
@@ -62,6 +60,7 @@ local function do_copy(source, destination)
   end
 
   if source_stats.type == "file" then
+    local success
     success, errmsg = vim.loop.fs_copyfile(source, destination)
     if not success then
       log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg)
@@ -69,6 +68,7 @@ local function do_copy(source, destination)
     end
     return true
   elseif source_stats.type == "directory" then
+    local handle
     handle, errmsg = vim.loop.fs_scandir(source)
     if type(handle) == "string" then
       return false, handle
@@ -77,6 +77,7 @@ local function do_copy(source, destination)
       return false, errmsg
     end
 
+    local success
     success, errmsg = vim.loop.fs_mkdir(destination, source_stats.mode)
     if not success then
       log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg)

From 3ec180ee052963f58caa33febcdbc435a106cd65 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Mon, 28 Oct 2024 17:52:22 +1100
Subject: [PATCH 65/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/clipboard.lua | 111 +++++++++++--------------
 1 file changed, 48 insertions(+), 63 deletions(-)

diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua
index 23266d424c4..1e786f025db 100644
--- a/lua/nvim-tree/actions/fs/clipboard.lua
+++ b/lua/nvim-tree/actions/fs/clipboard.lua
@@ -12,10 +12,14 @@ local DirectoryNode = require("nvim-tree.node.directory")
 ---@alias ClipboardAction "copy" | "cut"
 ---@alias ClipboardData table<ClipboardAction, Node[]>
 
+---@alias ClipboardActionFn fun(source: string, dest: string): boolean, string?
+
 ---@class Clipboard to handle all actions.fs clipboard API
 ---@field config table hydrated user opts.filters
 ---@field private explorer Explorer
 ---@field private data ClipboardData
+---@field private clipboard_name string
+---@field private reg string
 local Clipboard = {}
 
 ---@param opts table user options
@@ -29,9 +33,10 @@ function Clipboard:new(opts, explorer)
       copy = {},
       cut = {},
     },
+    clipboard_name = opts.actions.use_system_clipboard and "system" or "neovim",
+    reg = opts.actions.use_system_clipboard and "+" or "1",
     config = {
       filesystem_watchers = opts.filesystem_watchers,
-      actions = opts.actions,
     },
   }
 
@@ -45,11 +50,11 @@ end
 ---@return boolean
 ---@return string|nil
 local function do_copy(source, destination)
-  local source_stats, errmsg = vim.loop.fs_stat(source)
+  local source_stats, err = vim.loop.fs_stat(source)
 
   if not source_stats then
-    log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg)
-    return false, errmsg
+    log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, err)
+    return false, err
   end
 
   log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination)
@@ -61,27 +66,27 @@ local function do_copy(source, destination)
 
   if source_stats.type == "file" then
     local success
-    success, errmsg = vim.loop.fs_copyfile(source, destination)
+    success, err = vim.loop.fs_copyfile(source, destination)
     if not success then
-      log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg)
-      return false, errmsg
+      log.line("copy_paste", "do_copy fs_copyfile failed '%s'", err)
+      return false, err
     end
     return true
   elseif source_stats.type == "directory" then
     local handle
-    handle, errmsg = vim.loop.fs_scandir(source)
+    handle, err = vim.loop.fs_scandir(source)
     if type(handle) == "string" then
       return false, handle
     elseif not handle then
-      log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, errmsg)
-      return false, errmsg
+      log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, err)
+      return false, err
     end
 
     local success
-    success, errmsg = vim.loop.fs_mkdir(destination, source_stats.mode)
+    success, err = vim.loop.fs_mkdir(destination, source_stats.mode)
     if not success then
-      log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg)
-      return false, errmsg
+      log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, err)
+      return false, err
     end
 
     while true do
@@ -92,15 +97,15 @@ local function do_copy(source, destination)
 
       local new_name = utils.path_join({ source, name })
       local new_destination = utils.path_join({ destination, name })
-      success, errmsg = do_copy(new_name, new_destination)
+      success, err = do_copy(new_name, new_destination)
       if not success then
-        return false, errmsg
+        return false, err
       end
     end
   else
-    errmsg = string.format("'%s' illegal file type '%s'", source, source_stats.type)
-    log.line("copy_paste", "do_copy %s", errmsg)
-    return false, errmsg
+    err = string.format("'%s' illegal file type '%s'", source, source_stats.type)
+    log.line("copy_paste", "do_copy %s", err)
+    return false, err
   end
 
   return true
@@ -109,27 +114,25 @@ end
 ---@param source string
 ---@param dest string
 ---@param action ClipboardAction
----@param action_fn fun(source: string, dest: string)
+---@param action_fn ClipboardActionFn
 ---@return boolean|nil -- success
 ---@return string|nil -- error message
 local function do_single_paste(source, dest, action, action_fn)
-  local dest_stats
-  local success, errmsg, errcode
   local notify_source = notify.render_path(source)
 
   log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest)
 
-  dest_stats, errmsg, errcode = vim.loop.fs_stat(dest)
-  if not dest_stats and errcode ~= "ENOENT" then
-    notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???"))
-    return false, errmsg
+  local dest_stats, err, err_name = vim.loop.fs_stat(dest)
+  if not dest_stats and err_name ~= "ENOENT" then
+    notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (err or "???"))
+    return false, err
   end
 
   local function on_process()
-    success, errmsg = action_fn(source, dest)
+    local success, error = action_fn(source, dest)
     if not success then
-      notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???"))
-      return false, errmsg
+      notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???"))
+      return false, error
     end
 
     find_file(utils.path_remove_trailing(dest))
@@ -216,7 +219,7 @@ end
 ---@private
 ---@param node Node
 ---@param action ClipboardAction
----@param action_fn fun(source: string, dest: string)
+---@param action_fn ClipboardActionFn
 function Clipboard:do_paste(node, action, action_fn)
   if node.name == ".." then
     node = self.explorer
@@ -232,10 +235,10 @@ function Clipboard:do_paste(node, action, action_fn)
   end
 
   local destination = node.absolute_path
-  local stats, errmsg, errcode = vim.loop.fs_stat(destination)
-  if not stats and errcode ~= "ENOENT" then
-    log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, errmsg)
-    notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (errmsg or "???"))
+  local stats, err, err_name = vim.loop.fs_stat(destination)
+  if not stats and err_name ~= "ENOENT" then
+    log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err)
+    notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???"))
     return
   end
   local is_dir = stats and stats.type == "directory"
@@ -307,65 +310,45 @@ end
 
 ---@param content string
 function Clipboard:copy_to_reg(content)
-  local clipboard_name
-  local reg
-  if self.config.actions.use_system_clipboard == true then
-    clipboard_name = "system"
-    reg = "+"
-  else
-    clipboard_name = "neovim"
-    reg = "1"
-  end
-
   -- manually firing TextYankPost does not set vim.v.event
   -- workaround: create a scratch buffer with the clipboard contents and send a yank command
   local temp_buf = vim.api.nvim_create_buf(false, true)
   vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content })
   vim.api.nvim_buf_call(temp_buf, function()
-    vim.cmd(string.format('normal! "%sy$', reg))
+    vim.cmd(string.format('normal! "%sy$', self.reg))
   end)
   vim.api.nvim_buf_delete(temp_buf, {})
 
-  notify.info(string.format("Copied %s to %s clipboard!", content, clipboard_name))
+  notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
 end
 
 ---@param node Node
 function Clipboard:copy_filename(node)
-  local content
-
   if node.name == ".." then
     -- root
-    content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t")
+    self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t"))
   else
     -- node
-    content = node.name
+    self:copy_to_reg(node.name)
   end
-
-  self:copy_to_reg(content)
 end
 
 ---@param node Node
 function Clipboard:copy_basename(node)
-  local content
-
   if node.name == ".." then
     -- root
-    content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r")
+    self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r"))
   else
     -- node
-    content = vim.fn.fnamemodify(node.name, ":r")
+    self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
   end
-
-  self:copy_to_reg(content)
 end
 
 ---@param node Node
 function Clipboard:copy_path(node)
-  local content
-
   if node.name == ".." then
     -- root
-    content = utils.path_add_trailing("")
+    self:copy_to_reg(utils.path_add_trailing(""))
   else
     -- node
     local absolute_path = node.absolute_path
@@ -375,10 +358,12 @@ function Clipboard:copy_path(node)
     end
 
     local relative_path = utils.path_relative(absolute_path, cwd)
-    content = node:is(DirectoryNode) and utils.path_add_trailing(relative_path) or relative_path
+    if node:is(DirectoryNode) then
+      self:copy_to_reg(utils.path_add_trailing(relative_path))
+    else
+      self:copy_to_reg(relative_path)
+    end
   end
-
-  self:copy_to_reg(content)
 end
 
 ---@param node Node

From 217370b5546e6c7a3009afa9eed8ced21038a54b Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 10:10:56 +1100
Subject: [PATCH 66/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/components/icons.lua | 35 +++++++++++----------
 1 file changed, 19 insertions(+), 16 deletions(-)

diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 93f1a7bffca..70bc5780974 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -1,3 +1,5 @@
+local DirectoryLinkNode = require("nvim-tree.node.directory-link")
+
 local M = { i = {} }
 
 local function config_symlinks()
@@ -11,39 +13,40 @@ local function empty()
   return "", nil
 end
 
----@param node Node
+---@param dir DirectoryNode
 ---@param has_children boolean
 ---@return string icon
 ---@return string? name
-local function get_folder_icon_default(node, has_children)
-  local is_symlink = node.links_to ~= nil
-  local n
-  if is_symlink and node.open then
-    n = M.config.glyphs.folder.symlink_open
-  elseif is_symlink then
-    n = M.config.glyphs.folder.symlink
-  elseif node.open then
+local function get_folder_icon_default(dir, has_children)
+  local icon
+  if dir:is(DirectoryLinkNode) then
+    if dir.open then
+      icon = M.config.glyphs.folder.symlink_open
+    else
+      icon = M.config.glyphs.folder.symlink
+    end
+  elseif dir.open then
     if has_children then
-      n = M.config.glyphs.folder.open
+      icon = M.config.glyphs.folder.open
     else
-      n = M.config.glyphs.folder.empty_open
+      icon = M.config.glyphs.folder.empty_open
     end
   else
     if has_children then
-      n = M.config.glyphs.folder.default
+      icon = M.config.glyphs.folder.default
     else
-      n = M.config.glyphs.folder.empty
+      icon = M.config.glyphs.folder.empty
     end
   end
-  return n, nil
+  return icon, nil
 end
 
----@param node Node
+---@param node DirectoryNode
 ---@param has_children boolean
 ---@return string icon
 ---@return string? name
 local function get_folder_icon_webdev(node, has_children)
-  local icon, hl_group = M.devicons.get_icon(node.name, node.extension)
+  local icon, hl_group = M.devicons.get_icon(node.name, nil)
   if not M.config.web_devicons.folder.color then
     hl_group = nil
   end

From e38c38716024517be99c1dbe441854d54a20da9b Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 11:12:06 +1100
Subject: [PATCH 67/88] chore: resolve undefined-field

---
 lua/nvim-tree/api.lua            | 16 +++++++++-------
 lua/nvim-tree/node/directory.lua |  2 +-
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua
index bbb023a1aef..85762656a1d 100644
--- a/lua/nvim-tree/api.lua
+++ b/lua/nvim-tree/api.lua
@@ -9,6 +9,7 @@ local keymap = require("nvim-tree.keymap")
 local notify = require("nvim-tree.notify")
 
 local DirectoryNode = require("nvim-tree.node.directory")
+local FileLinkNode = require("nvim-tree.node.file-link")
 local RootNode = require("nvim-tree.node.root")
 
 local Api = {
@@ -206,10 +207,8 @@ Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_pa
 ---@param mode string
 ---@param node Node
 local function edit(mode, node)
-  local path = node.absolute_path
-  if node.link_to and not node.nodes then
-    path = node.link_to
-  end
+  local file_link = node:as(FileLinkNode)
+  local path = file_link and file_link.link_to or node.absolute_path
   actions.node.open_file.fn(mode, path)
 end
 
@@ -219,10 +218,13 @@ end
 local function open_or_expand_or_dir_up(mode, toggle_group)
   ---@param node Node
   return function(node)
-    if node.name == ".." then
+    local root = node:as(RootNode)
+    local dir = node:as(DirectoryNode)
+
+    if root or node.name == ".." then
       actions.root.change_dir.fn("..")
-    elseif node:is(DirectoryNode) then
-      node:expand_or_collapse(toggle_group)
+    elseif dir then
+      dir:expand_or_collapse(toggle_group)
     elseif not toggle_group then
       edit(mode, node)
     end
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 7daa32a8055..8bf659a001a 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -174,7 +174,7 @@ function DirectoryNode:ungroup_empty_folders()
   end
 end
 
----@param toggle_group boolean
+---@param toggle_group boolean?
 function DirectoryNode:expand_or_collapse(toggle_group)
   toggle_group = toggle_group or false
   if self.has_children then

From 0e9b844d22c737fa05eef2a72c2010d38a34ddec Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 11:19:45 +1100
Subject: [PATCH 68/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/builder.lua | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua
index 104abb08dc2..60e89e3d90f 100644
--- a/lua/nvim-tree/renderer/builder.lua
+++ b/lua/nvim-tree/renderer/builder.lua
@@ -142,7 +142,7 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
 end
 
 ---@private
----@param node Node
+---@param node DirectoryNode
 ---@return HighlightedString icon
 ---@return HighlightedString name
 function Builder:build_folder(node)
@@ -352,15 +352,19 @@ end
 ---@param idx integer line number starting at 1
 ---@param num_children integer of node
 function Builder:build_line(node, idx, num_children)
+  local dir = node:as(DirectoryNode)
+  local dir_link = node:as(FileLinkNode)
+  local file_link = node:as(DirectoryLinkNode)
+
   -- various components
   local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers)
   local arrows = pad.get_arrows(node)
 
   -- main components
   local icon, name
-  if node:is(DirectoryNode) then
-    icon, name = self:build_folder(node)
-  elseif node:is(DirectoryLinkNode) or node:is(FileLinkNode) then
+  if dir then
+    icon, name = self:build_folder(dir)
+  elseif dir_link or file_link then
     icon, name = self:build_symlink(node)
   else
     icon, name = self:build_file(node)

From 05f4376605b0c08462373446ad4fdbc72e37c2f7 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 11:47:01 +1100
Subject: [PATCH 69/88] Revert "chore: resolve undefined-field"

This reverts commit 0e9b844d22c737fa05eef2a72c2010d38a34ddec.
---
 lua/nvim-tree/renderer/builder.lua | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua
index 60e89e3d90f..104abb08dc2 100644
--- a/lua/nvim-tree/renderer/builder.lua
+++ b/lua/nvim-tree/renderer/builder.lua
@@ -142,7 +142,7 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
 end
 
 ---@private
----@param node DirectoryNode
+---@param node Node
 ---@return HighlightedString icon
 ---@return HighlightedString name
 function Builder:build_folder(node)
@@ -352,19 +352,15 @@ end
 ---@param idx integer line number starting at 1
 ---@param num_children integer of node
 function Builder:build_line(node, idx, num_children)
-  local dir = node:as(DirectoryNode)
-  local dir_link = node:as(FileLinkNode)
-  local file_link = node:as(DirectoryLinkNode)
-
   -- various components
   local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers)
   local arrows = pad.get_arrows(node)
 
   -- main components
   local icon, name
-  if dir then
-    icon, name = self:build_folder(dir)
-  elseif dir_link or file_link then
+  if node:is(DirectoryNode) then
+    icon, name = self:build_folder(node)
+  elseif node:is(DirectoryLinkNode) or node:is(FileLinkNode) then
     icon, name = self:build_symlink(node)
   else
     icon, name = self:build_file(node)

From 31545f2801642ce077274ab9a64cb9cde5291184 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 12:54:23 +1100
Subject: [PATCH 70/88] move icon builders into node classes

---
 lua/nvim-tree/node/directory-link.lua       |  17 +++
 lua/nvim-tree/node/directory.lua            |  48 ++++++++
 lua/nvim-tree/node/file-link.lua            |  17 +++
 lua/nvim-tree/node/file.lua                 |  27 +++++
 lua/nvim-tree/node/init.lua                 |   8 ++
 lua/nvim-tree/renderer/builder.lua          | 123 +-------------------
 lua/nvim-tree/renderer/components/icons.lua |   3 +-
 7 files changed, 125 insertions(+), 118 deletions(-)

diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 8e19a9ee98d..918c4b76020 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -1,4 +1,6 @@
 local git_utils = require("nvim-tree.git.utils")
+local icons = require("nvim-tree.renderer.components.icons")
+local utils = require("nvim-tree.utils")
 
 local DirectoryNode = require("nvim-tree.node.directory")
 
@@ -43,6 +45,21 @@ function DirectoryLinkNode:update_git_status(parent_ignored, project)
   self.git_status = git_utils.git_status_dir(parent_ignored, project, self.link_to, self.absolute_path)
 end
 
+---Icon and name for the directory link
+---@return HighlightedString icon
+---@return HighlightedString name
+function DirectoryLinkNode:icon_name()
+  local icon, name = DirectoryNode.icon_name(self)
+
+  if self.explorer.opts.renderer.symlink_destination then
+    local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
+    icon.hl = { string.format("%s%s%s", name.str, icons.i.symlink_arrow, link_to) }
+    name.hl = { "NvimTreeSymlinkFolderName" }
+  end
+
+  return icon, name
+end
+
 ---Create a sanitized partial copy of a node, populating children recursively.
 ---@return DirectoryLinkNode cloned
 function DirectoryLinkNode:clone()
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 8bf659a001a..502d4b0d77a 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,4 +1,6 @@
 local git_utils = require("nvim-tree.git.utils")
+local icons = require("nvim-tree.renderer.components.icons")
+local notify = require("nvim-tree.notify")
 
 local Node = require("nvim-tree.node")
 
@@ -207,6 +209,52 @@ function DirectoryNode:expand_or_collapse(toggle_group)
   self.explorer.renderer:draw()
 end
 
+---Icon and name for the directory
+---@return HighlightedString icon
+---@return HighlightedString name
+function DirectoryNode:icon_name()
+  local has_children = #self.nodes ~= 0 or self.has_children
+  local icon, icon_hl = icons.get_folder_icon(self, has_children)
+
+  local name = self.name
+  local next = self.group_next
+  while next do
+    name = string.format("%s/%s", name, next.name)
+    next = next.group_next
+  end
+
+  if self.group_next and type(self.explorer.opts.renderer.group_empty) == "function" then
+    local new_name = self.explorer.opts.renderer.group_empty(name)
+    if type(new_name) == "string" then
+      name = new_name
+    else
+      notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name)))
+    end
+  end
+
+  local foldername = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
+  foldername = name
+
+  if #icon > 0 and icon_hl == nil then
+    if self.open then
+      icon_hl = "NvimTreeOpenedFolderIcon"
+    else
+      icon_hl = "NvimTreeClosedFolderIcon"
+    end
+  end
+
+  local foldername_hl = "NvimTreeFolderName"
+  if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
+    foldername_hl = "NvimTreeSpecialFolderName"
+  elseif self.open then
+    foldername_hl = "NvimTreeOpenedFolderName"
+  elseif not has_children then
+    foldername_hl = "NvimTreeEmptyFolderName"
+  end
+
+  return { str = icon, hl = { icon_hl } }, { str = foldername, hl = { foldername_hl } }
+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/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 1faa2f04208..d5e92d6da0c 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -1,4 +1,6 @@
 local git_utils = require("nvim-tree.git.utils")
+local icons = require("nvim-tree.renderer.components.icons")
+local utils = require("nvim-tree.utils")
 
 local FileNode = require("nvim-tree.node.file")
 
@@ -39,6 +41,21 @@ function FileLinkNode:update_git_status(parent_ignored, project)
   self.git_status = git_utils.git_status_file(parent_ignored, project, self.link_to, self.absolute_path)
 end
 
+---Icon and name for the file link
+---@return HighlightedString icon
+---@return HighlightedString name
+function FileLinkNode:icon_name()
+  local icon = icons.i.symlink
+  local arrow = icons.i.symlink_arrow
+  local symlink_formatted = self.name
+  if self.explorer.opts.renderer.symlink_destination then
+    local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
+    symlink_formatted = string.format("%s%s%s", symlink_formatted, arrow, link_to)
+  end
+
+  return { str = icon, hl = { "NvimTreeSymlinkIcon" } }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } }
+end
+
 ---Create a sanitized partial copy of a node
 ---@return FileLinkNode cloned
 function FileLinkNode:clone()
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 4f0216fe6d0..9f5b81c29af 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -1,8 +1,18 @@
 local git_utils = require("nvim-tree.git.utils")
+local icons = require("nvim-tree.renderer.components.icons")
 local utils = require("nvim-tree.utils")
 
 local Node = require("nvim-tree.node")
 
+local PICTURE_MAP = {
+  jpg = true,
+  jpeg = true,
+  png = true,
+  gif = true,
+  webp = true,
+  jxl = true,
+}
+
 ---@class (exact) FileNode: Node
 ---@field extension string
 local FileNode = Node:new()
@@ -56,6 +66,23 @@ function FileNode:get_git_xy()
   return self.git_status.file and { self.git_status.file }
 end
 
+---Icon and name for the file
+---@return HighlightedString icon
+---@return HighlightedString name
+function FileNode:icon_name()
+  local hl
+  if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
+    hl = "NvimTreeSpecialFile"
+  elseif self.executable then
+    hl = "NvimTreeExecFile"
+  elseif PICTURE_MAP[self.extension] then
+    hl = "NvimTreeImageFile"
+  end
+
+  local icon, hl_group = icons.get_file_icon(self.name, self.extension)
+  return { str = icon, hl = { hl_group } }, { str = self.name, hl = { hl } }
+end
+
 ---Create a sanitized partial copy of a node
 ---@return FileNode cloned
 function FileNode:clone()
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index af6770c3592..788c35a9ecf 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -68,6 +68,14 @@ function Node:get_parent_of_group()
   end
 end
 
+---Icon and name for the node
+---Empty for base Node
+---@return HighlightedString icon
+---@return HighlightedString name
+function Node:icon_name()
+  return { str = "", hl = {} }, { str = self.name, hl = {} }
+end
+
 ---Create a sanitized partial copy of a node, populating children recursively.
 ---@return Node cloned
 function Node:clone()
diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua
index 104abb08dc2..a5397256cfd 100644
--- a/lua/nvim-tree/renderer/builder.lua
+++ b/lua/nvim-tree/renderer/builder.lua
@@ -2,9 +2,7 @@ local notify = require("nvim-tree.notify")
 local utils = require("nvim-tree.utils")
 local view = require("nvim-tree.view")
 
-local DirectoryLinkNode = require("nvim-tree.node.directory-link")
 local DirectoryNode = require("nvim-tree.node.directory")
-local FileLinkNode = require("nvim-tree.node.file-link")
 
 local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks")
 local DecoratorCopied = require("nvim-tree.renderer.decorator.copied")
@@ -16,16 +14,6 @@ local DecoratorHidden = require("nvim-tree.renderer.decorator.hidden")
 local DecoratorOpened = require("nvim-tree.renderer.decorator.opened")
 
 local pad = require("nvim-tree.renderer.components.padding")
-local icons = require("nvim-tree.renderer.components.icons")
-
-local PICTURE_MAP = {
-  jpg = true,
-  jpeg = true,
-  png = true,
-  gif = true,
-  webp = true,
-  jxl = true,
-}
 
 ---@class (exact) HighlightedString
 ---@field str string
@@ -100,27 +88,6 @@ function Builder:insert_highlight(groups, start, end_)
   table.insert(self.hl_args, { groups, self.index, start, end_ or -1 })
 end
 
----@private
-function Builder:get_folder_name(node)
-  local name = node.name
-  local next = node.group_next
-  while next do
-    name = string.format("%s/%s", name, next.name)
-    next = next.group_next
-  end
-
-  if node.group_next and type(self.opts.renderer.group_empty) == "function" then
-    local new_name = self.opts.renderer.group_empty(name)
-    if type(new_name) == "string" then
-      name = new_name
-    else
-      notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name)))
-    end
-  end
-
-  return string.format("%s%s", name, self.opts.renderer.add_trailing and "/" or "")
-end
-
 ---@private
 ---@param highlighted_strings HighlightedString[]
 ---@return string
@@ -141,78 +108,6 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
   return string
 end
 
----@private
----@param node Node
----@return HighlightedString icon
----@return HighlightedString name
-function Builder:build_folder(node)
-  local has_children = #node.nodes ~= 0 or node.has_children
-  local icon, icon_hl = icons.get_folder_icon(node, has_children)
-  local foldername = self:get_folder_name(node)
-
-  if #icon > 0 and icon_hl == nil then
-    if node.open then
-      icon_hl = "NvimTreeOpenedFolderIcon"
-    else
-      icon_hl = "NvimTreeClosedFolderIcon"
-    end
-  end
-
-  local foldername_hl = "NvimTreeFolderName"
-  if node.link_to and self.opts.renderer.symlink_destination then
-    local arrow = icons.i.symlink_arrow
-    local link_to = utils.path_relative(node.link_to, self.explorer.absolute_path)
-    foldername = string.format("%s%s%s", foldername, arrow, link_to)
-    foldername_hl = "NvimTreeSymlinkFolderName"
-  elseif
-    vim.tbl_contains(self.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(self.opts.renderer.special_files, node.name)
-  then
-    foldername_hl = "NvimTreeSpecialFolderName"
-  elseif node.open then
-    foldername_hl = "NvimTreeOpenedFolderName"
-  elseif not has_children then
-    foldername_hl = "NvimTreeEmptyFolderName"
-  end
-
-  return { str = icon, hl = { icon_hl } }, { str = foldername, hl = { foldername_hl } }
-end
-
----@private
----@param node table
----@return HighlightedString icon
----@return HighlightedString name
-function Builder:build_symlink(node)
-  local icon = icons.i.symlink
-  local arrow = icons.i.symlink_arrow
-  local symlink_formatted = node.name
-  if self.opts.renderer.symlink_destination then
-    local link_to = utils.path_relative(node.link_to, self.explorer.absolute_path)
-    symlink_formatted = string.format("%s%s%s", symlink_formatted, arrow, link_to)
-  end
-
-  return { str = icon, hl = { "NvimTreeSymlinkIcon" } }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } }
-end
-
----@private
----@param node Node
----@return HighlightedString icon
----@return HighlightedString name
-function Builder:build_file(node)
-  local hl
-  if
-    vim.tbl_contains(self.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(self.opts.renderer.special_files, node.name)
-  then
-    hl = "NvimTreeSpecialFile"
-  elseif node.executable then
-    hl = "NvimTreeExecFile"
-  elseif PICTURE_MAP[node.extension] then
-    hl = "NvimTreeImageFile"
-  end
-
-  local icon, hl_group = icons.get_file_icon(node.name, node.extension)
-  return { str = icon, hl = { hl_group } }, { str = node.name, hl = { hl } }
-end
-
 ---@private
 ---@param indent_markers HighlightedString[]
 ---@param arrows HighlightedString[]|nil
@@ -357,14 +252,7 @@ function Builder:build_line(node, idx, num_children)
   local arrows = pad.get_arrows(node)
 
   -- main components
-  local icon, name
-  if node:is(DirectoryNode) then
-    icon, name = self:build_folder(node)
-  elseif node:is(DirectoryLinkNode) or node:is(FileLinkNode) then
-    icon, name = self:build_symlink(node)
-  else
-    icon, name = self:build_file(node)
-  end
+  local icon, name = node:icon_name()
 
   -- highighting
   local icon_hl_group, name_hl_group = self:add_highlights(node)
@@ -376,11 +264,12 @@ function Builder:build_line(node, idx, num_children)
 
   self.index = self.index + 1
 
-  if node:is(DirectoryNode) then
-    node = node:last_group_node()
-    if node.open then
+  local dir = node:as(DirectoryNode)
+  if dir then
+    dir = dir:last_group_node()
+    if dir.open then
       self.depth = self.depth + 1
-      self:build_lines(node)
+      self:build_lines(dir)
       self.depth = self.depth - 1
     end
   end
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 70bc5780974..4d05f4522a2 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -1,4 +1,4 @@
-local DirectoryLinkNode = require("nvim-tree.node.directory-link")
+local DirectoryLinkNode = nil --circular dependency
 
 local M = { i = {} }
 
@@ -127,6 +127,7 @@ function M.setup(opts)
   M.config = opts.renderer.icons
 
   M.devicons = pcall(require, "nvim-web-devicons") and require("nvim-web-devicons") or nil
+  DirectoryLinkNode = require("nvim-tree.node.directory-link")
 end
 
 return M

From a035ba52bb1032e31fbea41b59dc489134444b2c Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 13:02:58 +1100
Subject: [PATCH 71/88] move icon builders into node classes

---
 lua/nvim-tree/node/directory.lua | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 502d4b0d77a..28858c6c1e5 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -233,7 +233,6 @@ function DirectoryNode:icon_name()
   end
 
   local foldername = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
-  foldername = name
 
   if #icon > 0 and icon_hl == nil then
     if self.open then

From 86090374d58cde72a1a9f98cf5ca1842ad3f3bb1 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 15:33:32 +1100
Subject: [PATCH 72/88] chore: resolve undefined-field

---
 lua/nvim-tree/node/directory-link.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 918c4b76020..58503e81120 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -45,7 +45,7 @@ function DirectoryLinkNode:update_git_status(parent_ignored, project)
   self.git_status = git_utils.git_status_dir(parent_ignored, project, self.link_to, self.absolute_path)
 end
 
----Icon and name for the directory link
+---Maybe overrides name
 ---@return HighlightedString icon
 ---@return HighlightedString name
 function DirectoryLinkNode:icon_name()
@@ -53,7 +53,7 @@ function DirectoryLinkNode:icon_name()
 
   if self.explorer.opts.renderer.symlink_destination then
     local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
-    icon.hl = { string.format("%s%s%s", name.str, icons.i.symlink_arrow, link_to) }
+    name.str = string.format("%s%s%s", name.str, icons.i.symlink_arrow, link_to)
     name.hl = { "NvimTreeSymlinkFolderName" }
   end
 

From afc01ddc71b175f345891478dc1b0a2f8f806046 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 16:10:33 +1100
Subject: [PATCH 73/88] chore: resolve undefined-field

---
 lua/nvim-tree/node/directory.lua            | 32 ++++++++++-----------
 lua/nvim-tree/node/file-link.lua            | 10 +++----
 lua/nvim-tree/node/file.lua                 | 13 +++++----
 lua/nvim-tree/renderer/components/icons.lua | 22 +++++++-------
 4 files changed, 39 insertions(+), 38 deletions(-)

diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 28858c6c1e5..cb25041df7e 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -214,7 +214,16 @@ end
 ---@return HighlightedString name
 function DirectoryNode:icon_name()
   local has_children = #self.nodes ~= 0 or self.has_children
-  local icon, icon_hl = icons.get_folder_icon(self, has_children)
+
+  local icon_str, icon_hl = icons.get_folder_icon(self, has_children)
+
+  if #icon_str > 0 and icon_hl == nil then
+    if self.open then
+      icon_hl = "NvimTreeOpenedFolderIcon"
+    else
+      icon_hl = "NvimTreeClosedFolderIcon"
+    end
+  end
 
   local name = self.name
   local next = self.group_next
@@ -231,27 +240,18 @@ function DirectoryNode:icon_name()
       notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name)))
     end
   end
+  local name_str = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
 
-  local foldername = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
-
-  if #icon > 0 and icon_hl == nil then
-    if self.open then
-      icon_hl = "NvimTreeOpenedFolderIcon"
-    else
-      icon_hl = "NvimTreeClosedFolderIcon"
-    end
-  end
-
-  local foldername_hl = "NvimTreeFolderName"
+  local name_hl = "NvimTreeFolderName"
   if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
-    foldername_hl = "NvimTreeSpecialFolderName"
+    name_hl = "NvimTreeSpecialFolderName"
   elseif self.open then
-    foldername_hl = "NvimTreeOpenedFolderName"
+    name_hl = "NvimTreeOpenedFolderName"
   elseif not has_children then
-    foldername_hl = "NvimTreeEmptyFolderName"
+    name_hl = "NvimTreeEmptyFolderName"
   end
 
-  return { str = icon, hl = { icon_hl } }, { str = foldername, hl = { foldername_hl } }
+  return { str = icon_str, hl = { icon_hl } }, { str = name_str, hl = { name_hl } }
 end
 
 ---Create a sanitized partial copy of a node, populating children recursively.
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index d5e92d6da0c..360a5b8a5e7 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -45,15 +45,15 @@ end
 ---@return HighlightedString icon
 ---@return HighlightedString name
 function FileLinkNode:icon_name()
-  local icon = icons.i.symlink
-  local arrow = icons.i.symlink_arrow
-  local symlink_formatted = self.name
+  local icon_str = icons.i.symlink
+
+  local name_str = self.name
   if self.explorer.opts.renderer.symlink_destination then
     local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
-    symlink_formatted = string.format("%s%s%s", symlink_formatted, arrow, link_to)
+    name_str = string.format("%s%s%s", name_str, icons.i.symlink_arrow, link_to)
   end
 
-  return { str = icon, hl = { "NvimTreeSymlinkIcon" } }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } }
+  return { str = icon_str, hl = { "NvimTreeSymlinkIcon" } }, { str = name_str, hl = { "NvimTreeSymlink" } }
 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 9f5b81c29af..94a8f3b0fd4 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -70,17 +70,18 @@ end
 ---@return HighlightedString icon
 ---@return HighlightedString name
 function FileNode:icon_name()
-  local hl
+  local name_hl
   if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
-    hl = "NvimTreeSpecialFile"
+    name_hl = "NvimTreeSpecialFile"
   elseif self.executable then
-    hl = "NvimTreeExecFile"
+    name_hl = "NvimTreeExecFile"
   elseif PICTURE_MAP[self.extension] then
-    hl = "NvimTreeImageFile"
+    name_hl = "NvimTreeImageFile"
   end
 
-  local icon, hl_group = icons.get_file_icon(self.name, self.extension)
-  return { str = icon, hl = { hl_group } }, { str = self.name, hl = { hl } }
+  local icon_str, icon_hl = icons.get_file_icon(self.name, self.extension)
+
+  return { str = icon_str, hl = { icon_hl } }, { str = self.name, hl = { name_hl } }
 end
 
 ---Create a sanitized partial copy of a node
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 4d05f4522a2..41082ce99b7 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -8,7 +8,7 @@ local function config_symlinks()
 end
 
 ---@return string icon
----@return string? name
+---@return string? hl_group
 local function empty()
   return "", nil
 end
@@ -16,7 +16,7 @@ end
 ---@param dir DirectoryNode
 ---@param has_children boolean
 ---@return string icon
----@return string? name
+---@return string? hl_group
 local function get_folder_icon_default(dir, has_children)
   local icon
   if dir:is(DirectoryLinkNode) then
@@ -44,7 +44,7 @@ end
 ---@param node DirectoryNode
 ---@param has_children boolean
 ---@return string icon
----@return string? name
+---@return string? hl_group
 local function get_folder_icon_webdev(node, has_children)
   local icon, hl_group = M.devicons.get_icon(node.name, nil)
   if not M.config.web_devicons.folder.color then
@@ -58,7 +58,7 @@ local function get_folder_icon_webdev(node, has_children)
 end
 
 ---@return string icon
----@return string? name
+---@return string? hl_group
 local function get_file_icon_default()
   local hl_group = "NvimTreeFileIcon"
   local icon = M.config.glyphs.default
@@ -69,20 +69,20 @@ local function get_file_icon_default()
   end
 end
 
----@param fname string
----@param extension string
+---@param name string
+---@param ext string
 ---@return string icon
----@return string? name
-local function get_file_icon_webdev(fname, extension)
-  local icon, hl_group = M.devicons.get_icon(fname, extension)
+---@return string? hl_group
+local function get_file_icon_webdev(name, ext)
+  local icon, hl_group = M.devicons.get_icon(name, ext)
   if not M.config.web_devicons.file.color then
     hl_group = "NvimTreeFileIcon"
   end
   if icon and hl_group ~= "DevIconDefault" then
     return icon, hl_group
-  elseif string.match(extension, "%.(.*)") then
+  elseif string.match(ext, "%.(.*)") then
     -- If there are more extensions to the file, try to grab the icon for them recursively
-    return get_file_icon_webdev(fname, string.match(extension, "%.(.*)"))
+    return get_file_icon_webdev(name, string.match(ext, "%.(.*)"))
   else
     local devicons_default = M.devicons.get_default_icon()
     if devicons_default and type(devicons_default.icon) == "string" and type(devicons_default.name) == "string" then

From 3ec276c08be1b7410801c6cd967ec2f749753171 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 16:23:02 +1100
Subject: [PATCH 74/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/remove-file.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/actions/fs/remove-file.lua b/lua/nvim-tree/actions/fs/remove-file.lua
index 327a107122d..8a0f67cda3a 100644
--- a/lua/nvim-tree/actions/fs/remove-file.lua
+++ b/lua/nvim-tree/actions/fs/remove-file.lua
@@ -5,6 +5,9 @@ local view = require("nvim-tree.view")
 local lib = require("nvim-tree.lib")
 local notify = require("nvim-tree.notify")
 
+local DirectoryLinkNode = require("nvim-tree.node.directory-link")
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {
   config = {},
 }
@@ -89,7 +92,7 @@ end
 ---@param node Node
 function M.remove(node)
   local notify_node = notify.render_path(node.absolute_path)
-  if node.nodes ~= nil and not node.link_to then
+  if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then
     local success = remove_dir(node.absolute_path)
     if not success then
       notify.error("Could not remove " .. notify_node)

From 87932dc16b4e57dbd9d1c8cf92d7dbcb43965f6b Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 16:23:57 +1100
Subject: [PATCH 75/88] chore: resolve undefined-field

---
 lua/nvim-tree/actions/fs/trash.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/actions/fs/trash.lua b/lua/nvim-tree/actions/fs/trash.lua
index 15d96852a2b..c64dba0b72b 100644
--- a/lua/nvim-tree/actions/fs/trash.lua
+++ b/lua/nvim-tree/actions/fs/trash.lua
@@ -2,6 +2,9 @@ local core = require("nvim-tree.core")
 local lib = require("nvim-tree.lib")
 local notify = require("nvim-tree.notify")
 
+local DirectoryLinkNode = require("nvim-tree.node.directory-link")
+local DirectoryNode = require("nvim-tree.node.directory")
+
 local M = {
   config = {},
 }
@@ -54,7 +57,7 @@ function M.remove(node)
 
   local explorer = core.get_explorer()
 
-  if node.nodes ~= nil and not node.link_to then
+  if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then
     trash_path(function(_, rc)
       if rc ~= 0 then
         notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash")

From 33333a0b17f18951b4d416e6e7fa078ca430da92 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 16:28:45 +1100
Subject: [PATCH 76/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/components/padding.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/nvim-tree/renderer/components/padding.lua b/lua/nvim-tree/renderer/components/padding.lua
index 349438be827..ccb550e3229 100644
--- a/lua/nvim-tree/renderer/components/padding.lua
+++ b/lua/nvim-tree/renderer/components/padding.lua
@@ -64,7 +64,7 @@ end
 ---@param node Node
 ---@param markers table
 ---@param early_stop integer?
----@return HighlightedString[]
+---@return HighlightedString
 function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop)
   local str = ""
 

From 7988cb9882fa8116e1265feb4d6b56ad0f59d1f6 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 16:37:04 +1100
Subject: [PATCH 77/88] chore: resolve undefined-field

---
 lua/nvim-tree/renderer/components/icons.lua | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 41082ce99b7..0ce84cb31c9 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -1,6 +1,20 @@
 local DirectoryLinkNode = nil --circular dependency
 
-local M = { i = {} }
+---@class DevIcon
+---@field icon string
+---@field color string
+---@field cterm_color string
+---@field name string
+
+---@class DevIcons
+---@field get_icon fun(name: string, ext: string?): string?, string?
+---@field get_default_icon fun(): DevIcon
+
+local M = {
+  i = {},
+  ---@type DevIcons?
+  devicons = nil,
+}
 
 local function config_symlinks()
   M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink or ""

From 962f1ef25681fd6ff2e4185ca504332a6034c4cb Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Tue, 29 Oct 2024 17:50:46 +1100
Subject: [PATCH 78/88] move folder specifics from icons to Directory

---
 lua/nvim-tree/node/directory.lua            | 70 ++++++++++++++++-----
 lua/nvim-tree/renderer/components/icons.lua | 65 +++----------------
 2 files changed, 62 insertions(+), 73 deletions(-)

diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index cb25041df7e..a905ab5526a 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,7 +1,6 @@
 local git_utils = require("nvim-tree.git.utils")
 local icons = require("nvim-tree.renderer.components.icons")
 local notify = require("nvim-tree.notify")
-
 local Node = require("nvim-tree.node")
 
 ---@class (exact) DirectoryNode: Node
@@ -209,22 +208,53 @@ function DirectoryNode:expand_or_collapse(toggle_group)
   self.explorer.renderer:draw()
 end
 
----Icon and name for the directory
+---@private
 ---@return HighlightedString icon
----@return HighlightedString name
-function DirectoryNode:icon_name()
-  local has_children = #self.nodes ~= 0 or self.has_children
+function DirectoryNode:highlighted_icon()
+  local str, hl
+
+  -- devicon if enabled and available
+  if self.explorer.opts.renderer.icons.show.folder and self.explorer.opts.renderer.icons.web_devicons.folder.enable then
+    str, hl = icons.get_icon(self.name, nil)
+    if not self.explorer.opts.renderer.icons.web_devicons.folder.color then
+      hl = nil
+    end
+  end
 
-  local icon_str, icon_hl = icons.get_folder_icon(self, has_children)
+  -- default icon
+  if not str then
+    if #self.nodes ~= 0 or self.has_children then
+      if self.open then
+        str = self.explorer.opts.renderer.icons.glyphs.folder.open
+      else
+        str = self.explorer.opts.renderer.icons.glyphs.folder.default
+      end
+    else
+      if self.open then
+        str = self.explorer.opts.renderer.icons.glyphs.folder.empty_open
+      else
+        str = self.explorer.opts.renderer.icons.glyphs.folder.empty
+      end
+    end
+  end
 
-  if #icon_str > 0 and icon_hl == nil then
+  -- default color
+  if #str > 0 and hl == nil then
     if self.open then
-      icon_hl = "NvimTreeOpenedFolderIcon"
+      hl = "NvimTreeOpenedFolderIcon"
     else
-      icon_hl = "NvimTreeClosedFolderIcon"
+      hl = "NvimTreeClosedFolderIcon"
     end
   end
 
+  return { str = str, hl = { hl } }
+end
+
+---@private
+---@return HighlightedString icon
+function DirectoryNode:highlighted_name()
+  local str, hl
+
   local name = self.name
   local next = self.group_next
   while next do
@@ -240,18 +270,26 @@ function DirectoryNode:icon_name()
       notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name)))
     end
   end
-  local name_str = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
+  str = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
 
-  local name_hl = "NvimTreeFolderName"
+  hl = "NvimTreeFolderName"
   if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
-    name_hl = "NvimTreeSpecialFolderName"
+    hl = "NvimTreeSpecialFolderName"
   elseif self.open then
-    name_hl = "NvimTreeOpenedFolderName"
-  elseif not has_children then
-    name_hl = "NvimTreeEmptyFolderName"
+    hl = "NvimTreeOpenedFolderName"
+  elseif #self.nodes == 0 and not self.has_children then
+    hl = "NvimTreeEmptyFolderName"
   end
 
-  return { str = icon_str, hl = { icon_hl } }, { str = name_str, hl = { name_hl } }
+  return { str = str, hl = { hl } }
+end
+
+---TODO builder call highlighted_name and highlighted_icon separately
+---Highlighted icon and name for the directory
+---@return HighlightedString icon
+---@return HighlightedString name
+function DirectoryNode:icon_name()
+  return self:highlighted_icon(), self:highlighted_name()
 end
 
 ---Create a sanitized partial copy of a node, populating children recursively.
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 0ce84cb31c9..a9bc83cb8e4 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -1,5 +1,3 @@
-local DirectoryLinkNode = nil --circular dependency
-
 ---@class DevIcon
 ---@field icon string
 ---@field color string
@@ -27,50 +25,6 @@ local function empty()
   return "", nil
 end
 
----@param dir DirectoryNode
----@param has_children boolean
----@return string icon
----@return string? hl_group
-local function get_folder_icon_default(dir, has_children)
-  local icon
-  if dir:is(DirectoryLinkNode) then
-    if dir.open then
-      icon = M.config.glyphs.folder.symlink_open
-    else
-      icon = M.config.glyphs.folder.symlink
-    end
-  elseif dir.open then
-    if has_children then
-      icon = M.config.glyphs.folder.open
-    else
-      icon = M.config.glyphs.folder.empty_open
-    end
-  else
-    if has_children then
-      icon = M.config.glyphs.folder.default
-    else
-      icon = M.config.glyphs.folder.empty
-    end
-  end
-  return icon, nil
-end
-
----@param node DirectoryNode
----@param has_children boolean
----@return string icon
----@return string? hl_group
-local function get_folder_icon_webdev(node, has_children)
-  local icon, hl_group = M.devicons.get_icon(node.name, nil)
-  if not M.config.web_devicons.folder.color then
-    hl_group = nil
-  end
-  if icon ~= nil then
-    return icon, hl_group
-  else
-    return get_folder_icon_default(node, has_children)
-  end
-end
-
 ---@return string icon
 ---@return string? hl_group
 local function get_file_icon_default()
@@ -119,29 +73,26 @@ local function config_file_icon()
   end
 end
 
-local function config_folder_icon()
-  if M.config.show.folder then
-    if M.devicons and M.config.web_devicons.folder.enable then
-      M.get_folder_icon = get_folder_icon_webdev
-    else
-      M.get_folder_icon = get_folder_icon_default
-    end
-  else
-    M.get_folder_icon = empty
+---Wrapper around nvim-web-devicons, nil if not present
+---@param name string
+---@param ext string?
+---@return string? icon
+---@return string? hl_group
+function M.get_icon(name, ext)
+  if M.devicons then
+    return M.devicons.get_icon(name, ext)
   end
 end
 
 function M.reset_config()
   config_symlinks()
   config_file_icon()
-  config_folder_icon()
 end
 
 function M.setup(opts)
   M.config = opts.renderer.icons
 
   M.devicons = pcall(require, "nvim-web-devicons") and require("nvim-web-devicons") or nil
-  DirectoryLinkNode = require("nvim-tree.node.directory-link")
 end
 
 return M

From 0eb05a223d15d6b7e60215c3a70f9f89e1d1e2d0 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 2 Nov 2024 13:53:18 +1100
Subject: [PATCH 79/88] move folder specifics from icons to Directory

---
 lua/nvim-tree/node/directory-link.lua | 24 +++++++++++++++++++-----
 lua/nvim-tree/node/directory.lua      |  6 +++++-
 2 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 58503e81120..8a7c01e50f6 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -45,11 +45,10 @@ function DirectoryLinkNode:update_git_status(parent_ignored, project)
   self.git_status = git_utils.git_status_dir(parent_ignored, project, self.link_to, self.absolute_path)
 end
 
----Maybe overrides name
----@return HighlightedString icon
+---Maybe override name
 ---@return HighlightedString name
-function DirectoryLinkNode:icon_name()
-  local icon, name = DirectoryNode.icon_name(self)
+function DirectoryLinkNode:highlighted_name()
+  local name = DirectoryNode.highlighted_name(self)
 
   if self.explorer.opts.renderer.symlink_destination then
     local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
@@ -57,7 +56,22 @@ function DirectoryLinkNode:icon_name()
     name.hl = { "NvimTreeSymlinkFolderName" }
   end
 
-  return icon, name
+  return name
+end
+
+---@return HighlightedString name
+function DirectoryLinkNode:highlighted_icon()
+  local str, hl
+
+  if self.open then
+    str = self.explorer.opts.renderer.icons.glyphs.folder.symlink_open
+    hl = "NvimTreeOpenedFolderIcon"
+  else
+    str = self.explorer.opts.renderer.icons.glyphs.folder.symlink
+    hl = "NvimTreeClosedFolderIcon"
+  end
+
+  return { str = str, hl = { hl } }
 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 a905ab5526a..1cba921ae5d 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -211,6 +211,10 @@ end
 ---@private
 ---@return HighlightedString icon
 function DirectoryNode:highlighted_icon()
+  if not self.explorer.opts.renderer.icons.show.folder then
+    return { str = "", hl = {}, }
+  end
+
   local str, hl
 
   -- devicon if enabled and available
@@ -250,7 +254,7 @@ function DirectoryNode:highlighted_icon()
   return { str = str, hl = { hl } }
 end
 
----@private
+---@protected
 ---@return HighlightedString icon
 function DirectoryNode:highlighted_name()
   local str, hl

From 8053244d98470745c7941324b37fb14ee98ac4c2 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 2 Nov 2024 14:04:36 +1100
Subject: [PATCH 80/88] move folder specifics from icons to Directory

---
 lua/nvim-tree/node/directory-link.lua | 31 ++++++++++++++++-----------
 lua/nvim-tree/node/directory.lua      | 18 ++++------------
 lua/nvim-tree/node/init.lua           | 14 ++++++++----
 3 files changed, 32 insertions(+), 31 deletions(-)

diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 8a7c01e50f6..8f9138d71ce 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -3,6 +3,7 @@ local icons = require("nvim-tree.renderer.components.icons")
 local utils = require("nvim-tree.utils")
 
 local DirectoryNode = require("nvim-tree.node.directory")
+local Node = require("nvim-tree.node")
 
 ---@class (exact) DirectoryLinkNode: DirectoryNode
 ---@field link_to string absolute path
@@ -45,22 +46,12 @@ function DirectoryLinkNode:update_git_status(parent_ignored, project)
   self.git_status = git_utils.git_status_dir(parent_ignored, project, self.link_to, self.absolute_path)
 end
 
----Maybe override name
 ---@return HighlightedString name
-function DirectoryLinkNode:highlighted_name()
-  local name = DirectoryNode.highlighted_name(self)
-
-  if self.explorer.opts.renderer.symlink_destination then
-    local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
-    name.str = string.format("%s%s%s", name.str, icons.i.symlink_arrow, link_to)
-    name.hl = { "NvimTreeSymlinkFolderName" }
+function DirectoryLinkNode:highlighted_icon()
+  if not self.explorer.opts.renderer.icons.show.folder then
+    return Node.highlighted_icon(self)
   end
 
-  return name
-end
-
----@return HighlightedString name
-function DirectoryLinkNode:highlighted_icon()
   local str, hl
 
   if self.open then
@@ -74,6 +65,20 @@ function DirectoryLinkNode:highlighted_icon()
   return { str = str, hl = { hl } }
 end
 
+---Maybe override name with arrow
+---@return HighlightedString name
+function DirectoryLinkNode:highlighted_name()
+  local name = DirectoryNode.highlighted_name(self)
+
+  if self.explorer.opts.renderer.symlink_destination then
+    local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
+    name.str = string.format("%s%s%s", name.str, icons.i.symlink_arrow, link_to)
+    name.hl = { "NvimTreeSymlinkFolderName" }
+  end
+
+  return name
+end
+
 ---Create a sanitized partial copy of a node, populating children recursively.
 ---@return DirectoryLinkNode cloned
 function DirectoryLinkNode:clone()
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 1cba921ae5d..ed6f6ce6287 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -208,24 +208,23 @@ function DirectoryNode:expand_or_collapse(toggle_group)
   self.explorer.renderer:draw()
 end
 
----@private
 ---@return HighlightedString icon
 function DirectoryNode:highlighted_icon()
   if not self.explorer.opts.renderer.icons.show.folder then
-    return { str = "", hl = {}, }
+    return Node.highlighted_icon(self)
   end
 
   local str, hl
 
   -- devicon if enabled and available
-  if self.explorer.opts.renderer.icons.show.folder and self.explorer.opts.renderer.icons.web_devicons.folder.enable then
+  if self.explorer.opts.renderer.icons.web_devicons.folder.enable then
     str, hl = icons.get_icon(self.name, nil)
     if not self.explorer.opts.renderer.icons.web_devicons.folder.color then
       hl = nil
     end
   end
 
-  -- default icon
+  -- icon from opts
   if not str then
     if #self.nodes ~= 0 or self.has_children then
       if self.open then
@@ -242,7 +241,7 @@ function DirectoryNode:highlighted_icon()
     end
   end
 
-  -- default color
+  -- hl
   if #str > 0 and hl == nil then
     if self.open then
       hl = "NvimTreeOpenedFolderIcon"
@@ -254,7 +253,6 @@ function DirectoryNode:highlighted_icon()
   return { str = str, hl = { hl } }
 end
 
----@protected
 ---@return HighlightedString icon
 function DirectoryNode:highlighted_name()
   local str, hl
@@ -288,14 +286,6 @@ function DirectoryNode:highlighted_name()
   return { str = str, hl = { hl } }
 end
 
----TODO builder call highlighted_name and highlighted_icon separately
----Highlighted icon and name for the directory
----@return HighlightedString icon
----@return HighlightedString name
-function DirectoryNode:icon_name()
-  return self:highlighted_icon(), self:highlighted_name()
-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/init.lua b/lua/nvim-tree/node/init.lua
index 788c35a9ecf..3f8ff9cab04 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -68,12 +68,18 @@ function Node:get_parent_of_group()
   end
 end
 
----Icon and name for the node
+---Highlighted icon for the node
 ---Empty for base Node
 ---@return HighlightedString icon
----@return HighlightedString name
-function Node:icon_name()
-  return { str = "", hl = {} }, { str = self.name, hl = {} }
+function Node:highlighted_icon()
+  return { str = "", hl = {} }
+end
+
+---Highlighted name for the node
+---Empty for base Node
+---@return HighlightedString icon
+function Node:highlighted_name()
+  return { str = "", hl = {} }
 end
 
 ---Create a sanitized partial copy of a node, populating children recursively.

From 612f787d9755e36ba3660a9e4d43a3e66d7eb00d Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 2 Nov 2024 14:13:58 +1100
Subject: [PATCH 81/88] move folder specifics from icons to Directory

---
 lua/nvim-tree/node/file-link.lua   | 16 ++++++++++++++++
 lua/nvim-tree/node/file.lua        | 21 ++++++++++++---------
 lua/nvim-tree/renderer/builder.lua |  2 +-
 3 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 360a5b8a5e7..b733c99676b 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -41,6 +41,22 @@ function FileLinkNode:update_git_status(parent_ignored, project)
   self.git_status = git_utils.git_status_file(parent_ignored, project, self.link_to, self.absolute_path)
 end
 
+---@return HighlightedString icon
+function FileLinkNode:highlighted_icon()
+  return { str = icons.i.symlink, hl = { "NvimTreeSymlinkIcon" } }
+end
+
+---@return HighlightedString name
+function FileLinkNode:highlighted_name()
+  local str = self.name
+  if self.explorer.opts.renderer.symlink_destination then
+    local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
+    str = string.format("%s%s%s", str, icons.i.symlink_arrow, link_to)
+  end
+
+  return { str = str, hl = { "NvimTreeSymlink" } }
+end
+
 ---Icon and name for the file link
 ---@return HighlightedString icon
 ---@return HighlightedString name
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 94a8f3b0fd4..db5a8bc82e2 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -66,22 +66,25 @@ function FileNode:get_git_xy()
   return self.git_status.file and { self.git_status.file }
 end
 
----Icon and name for the file
 ---@return HighlightedString icon
+function FileNode:highlighted_icon()
+  local icon_str, icon_hl = icons.get_file_icon(self.name, self.extension)
+
+  return { str = icon_str, hl = { icon_hl } }
+end
+
 ---@return HighlightedString name
-function FileNode:icon_name()
-  local name_hl
+function FileNode:highlighted_name()
+  local hl
   if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
-    name_hl = "NvimTreeSpecialFile"
+    hl = "NvimTreeSpecialFile"
   elseif self.executable then
-    name_hl = "NvimTreeExecFile"
+    hl = "NvimTreeExecFile"
   elseif PICTURE_MAP[self.extension] then
-    name_hl = "NvimTreeImageFile"
+    hl = "NvimTreeImageFile"
   end
 
-  local icon_str, icon_hl = icons.get_file_icon(self.name, self.extension)
-
-  return { str = icon_str, hl = { icon_hl } }, { str = self.name, hl = { name_hl } }
+  return { str = self.name, hl = { hl } }
 end
 
 ---Create a sanitized partial copy of a node
diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua
index a5397256cfd..37359269822 100644
--- a/lua/nvim-tree/renderer/builder.lua
+++ b/lua/nvim-tree/renderer/builder.lua
@@ -252,7 +252,7 @@ function Builder:build_line(node, idx, num_children)
   local arrows = pad.get_arrows(node)
 
   -- main components
-  local icon, name = node:icon_name()
+  local icon, name = node:highlighted_icon(), node:highlighted_name()
 
   -- highighting
   local icon_hl_group, name_hl_group = self:add_highlights(node)

From b5f2b3a1d292ed9bd686491cb19209ebed69752a Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 2 Nov 2024 15:44:00 +1100
Subject: [PATCH 82/88] move file specifics from icons to File

---
 lua/nvim-tree/node/directory.lua            |  8 +-
 lua/nvim-tree/node/file-link.lua            | 15 +++-
 lua/nvim-tree/node/file.lua                 | 26 ++++++-
 lua/nvim-tree/renderer/components/icons.lua | 83 +++------------------
 lua/nvim-tree/renderer/components/init.lua  |  2 +-
 lua/nvim-tree/renderer/init.lua             |  3 -
 6 files changed, 52 insertions(+), 85 deletions(-)

diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index ed6f6ce6287..b6fe89ae7c6 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -218,13 +218,13 @@ function DirectoryNode:highlighted_icon()
 
   -- devicon if enabled and available
   if self.explorer.opts.renderer.icons.web_devicons.folder.enable then
-    str, hl = icons.get_icon(self.name, nil)
+    str, hl = icons.get_icon(self.name)
     if not self.explorer.opts.renderer.icons.web_devicons.folder.color then
       hl = nil
     end
   end
 
-  -- icon from opts
+  -- default icon from opts
   if not str then
     if #self.nodes ~= 0 or self.has_children then
       if self.open then
@@ -241,8 +241,8 @@ function DirectoryNode:highlighted_icon()
     end
   end
 
-  -- hl
-  if #str > 0 and hl == nil then
+  -- default hl
+  if not hl then
     if self.open then
       hl = "NvimTreeOpenedFolderIcon"
     else
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index b733c99676b..095c4cc5504 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -3,6 +3,7 @@ local icons = require("nvim-tree.renderer.components.icons")
 local utils = require("nvim-tree.utils")
 
 local FileNode = require("nvim-tree.node.file")
+local Node = require("nvim-tree.node")
 
 ---@class (exact) FileLinkNode: FileNode
 ---@field link_to string absolute path
@@ -43,7 +44,17 @@ end
 
 ---@return HighlightedString icon
 function FileLinkNode:highlighted_icon()
-  return { str = icons.i.symlink, hl = { "NvimTreeSymlinkIcon" } }
+  if not self.explorer.opts.renderer.icons.show.folder then
+    return Node.highlighted_icon(self)
+  end
+
+  local str, hl
+
+  -- default icon from opts
+  str = self.explorer.opts.renderer.icons.glyphs.symlink
+  hl = "NvimTreeSymlinkIcon"
+
+  return { str = str, hl = { hl } }
 end
 
 ---@return HighlightedString name
@@ -51,7 +62,7 @@ function FileLinkNode:highlighted_name()
   local str = self.name
   if self.explorer.opts.renderer.symlink_destination then
     local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
-    str = string.format("%s%s%s", str, icons.i.symlink_arrow, link_to)
+    str = string.format("%s%s%s", str, self.explorer.opts.renderer.icons.symlink_arrow, link_to)
   end
 
   return { str = str, hl = { "NvimTreeSymlink" } }
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index db5a8bc82e2..3333471921d 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -68,9 +68,31 @@ end
 
 ---@return HighlightedString icon
 function FileNode:highlighted_icon()
-  local icon_str, icon_hl = icons.get_file_icon(self.name, self.extension)
+  if not self.explorer.opts.renderer.icons.show.file then
+    return Node.highlighted_icon(self)
+  end
+
+  local str, hl
+
+  -- devicon if enabled and available
+  if self.explorer.opts.renderer.icons.web_devicons.file.enable then
+    str, hl = icons.get_icon(self.name)
+    if not self.explorer.opts.renderer.icons.web_devicons.file.color then
+      hl = nil
+    end
+  end
+
+  -- default icon from opts
+  if not str then
+    str = self.explorer.opts.renderer.icons.glyphs.default
+  end
+
+  -- default hl
+  if not hl then
+    hl = "NvimTreeFileIcon"
+  end
 
-  return { str = icon_str, hl = { icon_hl } }
+  return { str = str, hl = { hl } }
 end
 
 ---@return HighlightedString name
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index a9bc83cb8e4..54a8c4fb9a2 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -9,90 +9,27 @@
 ---@field get_default_icon fun(): DevIcon
 
 local M = {
-  i = {},
   ---@type DevIcons?
   devicons = nil,
 }
 
-local function config_symlinks()
-  M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink or ""
-  M.i.symlink_arrow = M.config.symlink_arrow
-end
-
----@return string icon
----@return string? hl_group
-local function empty()
-  return "", nil
-end
-
----@return string icon
----@return string? hl_group
-local function get_file_icon_default()
-  local hl_group = "NvimTreeFileIcon"
-  local icon = M.config.glyphs.default
-  if #icon > 0 then
-    return icon, hl_group
-  else
-    return "", nil
-  end
-end
-
----@param name string
----@param ext string
----@return string icon
----@return string? hl_group
-local function get_file_icon_webdev(name, ext)
-  local icon, hl_group = M.devicons.get_icon(name, ext)
-  if not M.config.web_devicons.file.color then
-    hl_group = "NvimTreeFileIcon"
-  end
-  if icon and hl_group ~= "DevIconDefault" then
-    return icon, hl_group
-  elseif string.match(ext, "%.(.*)") then
-    -- If there are more extensions to the file, try to grab the icon for them recursively
-    return get_file_icon_webdev(name, string.match(ext, "%.(.*)"))
-  else
-    local devicons_default = M.devicons.get_default_icon()
-    if devicons_default and type(devicons_default.icon) == "string" and type(devicons_default.name) == "string" then
-      return devicons_default.icon, "DevIcon" .. devicons_default.name
-    else
-      return get_file_icon_default()
-    end
-  end
-end
-
-local function config_file_icon()
-  if M.config.show.file then
-    if M.devicons and M.config.web_devicons.file.enable then
-      M.get_file_icon = get_file_icon_webdev
-    else
-      M.get_file_icon = get_file_icon_default
-    end
-  else
-    M.get_file_icon = empty
-  end
-end
-
----Wrapper around nvim-web-devicons, nil if not present
+---Wrapper around nvim-web-devicons, nils if not present
 ---@param name string
----@param ext string?
 ---@return string? icon
 ---@return string? hl_group
-function M.get_icon(name, ext)
+function M.get_icon(name)
   if M.devicons then
-    return M.devicons.get_icon(name, ext)
+    return M.devicons.get_icon(name, nil)
+  else
+    return nil, nil
   end
 end
 
-function M.reset_config()
-  config_symlinks()
-  config_file_icon()
-end
-
-function M.setup(opts)
-  M.config = opts.renderer.icons
-
-  M.devicons = pcall(require, "nvim-web-devicons") and require("nvim-web-devicons") or nil
+function M.setup()
+  local devicons_ok, devicons = pcall(require, "nvim-web-devicons")
+  if devicons_ok then
+    M.devicons = devicons
+  end
 end
 
 return M
diff --git a/lua/nvim-tree/renderer/components/init.lua b/lua/nvim-tree/renderer/components/init.lua
index dfb767e63d8..ff4ceafdcd3 100644
--- a/lua/nvim-tree/renderer/components/init.lua
+++ b/lua/nvim-tree/renderer/components/init.lua
@@ -8,7 +8,7 @@ M.padding = require("nvim-tree.renderer.components.padding")
 function M.setup(opts)
   M.diagnostics.setup(opts)
   M.full_name.setup(opts)
-  M.icons.setup(opts)
+  M.icons.setup()
   M.padding.setup(opts)
 end
 
diff --git a/lua/nvim-tree/renderer/init.lua b/lua/nvim-tree/renderer/init.lua
index 49a69d1b162..f61c4be4e5f 100644
--- a/lua/nvim-tree/renderer/init.lua
+++ b/lua/nvim-tree/renderer/init.lua
@@ -2,8 +2,6 @@ local log = require("nvim-tree.log")
 local view = require("nvim-tree.view")
 local events = require("nvim-tree.events")
 
-local icon_component = require("nvim-tree.renderer.components.icons")
-
 local Builder = require("nvim-tree.renderer.builder")
 
 local SIGN_GROUP = "NvimTreeRendererSigns"
@@ -107,7 +105,6 @@ function Renderer:draw()
   local profile = log.profile_start("draw")
 
   local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0)
-  icon_component.reset_config()
 
   local builder = Builder:new(self.opts, self.explorer):build()
 

From 82c868fbabef0199680226abdbb5baa3fbbf2406 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 2 Nov 2024 16:19:22 +1100
Subject: [PATCH 83/88] clean up sorters

---
 lua/nvim-tree/explorer/init.lua    |  2 +-
 lua/nvim-tree/explorer/sorters.lua | 52 +++++++++++++++++++-----------
 2 files changed, 35 insertions(+), 19 deletions(-)

diff --git a/lua/nvim-tree/explorer/init.lua b/lua/nvim-tree/explorer/init.lua
index 063ea316914..ecb042d2dd3 100644
--- a/lua/nvim-tree/explorer/init.lua
+++ b/lua/nvim-tree/explorer/init.lua
@@ -69,7 +69,7 @@ function Explorer:create(path)
   o.open = true
   o.opts = config
 
-  o.sorters = Sorters:new(config)
+  o.sorters = Sorters:create(config)
   o.renderer = Renderer:new(config, o)
   o.filters = Filters:new(config, o)
   o.live_filter = LiveFilter:new(config, o)
diff --git a/lua/nvim-tree/explorer/sorters.lua b/lua/nvim-tree/explorer/sorters.lua
index f9e00166b1b..15ed921da05 100644
--- a/lua/nvim-tree/explorer/sorters.lua
+++ b/lua/nvim-tree/explorer/sorters.lua
@@ -1,16 +1,32 @@
-local C = {}
-
----@class Sorter
-local Sorter = {}
+local Class = require("nvim-tree.class")
+local DirectoryNode = require("nvim-tree.node.directory")
 
-function Sorter:new(opts)
-  local o = {}
-  setmetatable(o, self)
-  self.__index = self
-  o.config = vim.deepcopy(opts.sort)
+local C = {}
 
-  if type(o.config.sorter) == "function" then
-    o.user = o.config.sorter
+---@class (exact) SorterCfg
+---@field sorter string|fun(nodes: Node[])
+---@field folders_first boolean
+---@field files_first boolean
+
+---@class (exact) Sorter: Class
+---@field cfg SorterCfg
+---@field user fun(nodes: Node[])?
+---@field pre string?
+local Sorter = Class:new()
+
+---@param opts table user options
+---@return Sorter
+function Sorter:create(opts)
+  ---@type Sorter
+  local o = {
+    cfg = vim.deepcopy(opts.sort),
+  }
+  o = self:new(o)
+
+  if type(o.cfg.sorter) == "function" then
+    o.user = o.cfg.sorter --[[@as fun(nodes: Node[])]]
+  elseif type(o.cfg.sorter) == "string" then
+    o.pre = o.cfg.sorter --[[@as string]]
   end
   return o
 end
@@ -20,7 +36,7 @@ end
 ---@return fun(a: Node, b: Node): boolean
 function Sorter:get_comparator(sorter)
   return function(a, b)
-    return (C[sorter] or C.name)(a, b, self.config)
+    return (C[sorter] or C.name)(a, b, self.cfg)
   end
 end
 
@@ -41,17 +57,17 @@ end
 ---Evaluate `sort.folders_first` and `sort.files_first`
 ---@param a Node
 ---@param b Node
----@param cfg table
+---@param cfg SorterCfg
 ---@return boolean|nil
 local function folders_or_files_first(a, b, cfg)
   if not (cfg.folders_first or cfg.files_first) then
     return
   end
 
-  if not a.nodes and b.nodes then
+  if not a:is(DirectoryNode) and b:is(DirectoryNode) then
     -- file <> folder
     return cfg.files_first
-  elseif a.nodes and not b.nodes then
+  elseif a:is(DirectoryNode) and not b:is(DirectoryNode) then
     -- folder <> file
     return not cfg.files_first
   end
@@ -157,15 +173,15 @@ function Sorter:sort(t)
     end
 
     split_merge(t, 1, #t, mini_comparator) -- sort by user order
-  else
-    split_merge(t, 1, #t, self:get_comparator(self.config.sorter))
+  elseif self.pre then
+    split_merge(t, 1, #t, self:get_comparator(self.pre))
   end
 end
 
 ---@param a Node
 ---@param b Node
 ---@param ignorecase boolean|nil
----@param cfg table
+---@param cfg SorterCfg
 ---@return boolean
 local function node_comparator_name_ignorecase_or_not(a, b, ignorecase, cfg)
   if not (a and b) then

From 1e1a273b36a4aa05664decbef98588a2333db232 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sat, 2 Nov 2024 17:16:35 +1100
Subject: [PATCH 84/88] chore: resolve undefined-field

---
 lua/nvim-tree/log.lua | 37 +++++++++++++++++++++----------------
 1 file changed, 21 insertions(+), 16 deletions(-)

diff --git a/lua/nvim-tree/log.lua b/lua/nvim-tree/log.lua
index bfb968ae5ca..9665c1335e6 100644
--- a/lua/nvim-tree/log.lua
+++ b/lua/nvim-tree/log.lua
@@ -1,7 +1,12 @@
-local M = {
-  config = nil,
-  path = nil,
-}
+---@alias LogTypes "all" | "config" | "copy_paste" | "dev" | "diagnostics" | "git" | "profile" | "watcher"
+
+---@type table<LogTypes, boolean>
+local types = {}
+
+---@type string
+local file_path
+
+local M = {}
 
 --- Write to log file
 ---@param typ string as per log.types config
@@ -13,7 +18,7 @@ function M.raw(typ, fmt, ...)
   end
 
   local line = string.format(fmt, ...)
-  local file = io.open(M.path, "a")
+  local file = io.open(file_path, "a")
   if file then
     io.output(file)
     io.write(line)
@@ -22,7 +27,7 @@ function M.raw(typ, fmt, ...)
 end
 
 --- Write to a new file
----@param typ string as per log.types config
+---@param typ LogTypes as per log.types config
 ---@param path string absolute path
 ---@param fmt string for string.format
 ---@param ... any arguments for string.format
@@ -71,7 +76,7 @@ end
 
 --- Write to log file
 --- time and typ are prefixed and a trailing newline is added
----@param typ string as per log.types config
+---@param typ LogTypes as per log.types config
 ---@param fmt string for string.format
 ---@param ... any arguments for string.format
 function M.line(typ, fmt, ...)
@@ -88,7 +93,7 @@ function M.set_inspect_opts(opts)
 end
 
 --- Write to log file the inspection of a node
----@param typ string as per log.types config
+---@param typ LogTypes as per log.types config
 ---@param node Node node to be inspected
 ---@param fmt string for string.format
 ---@param ... any arguments for string.format
@@ -99,20 +104,20 @@ function M.node(typ, node, fmt, ...)
 end
 
 --- Logging is enabled for typ or all
----@param typ string as per log.types config
+---@param typ LogTypes as per log.types config
 ---@return boolean
 function M.enabled(typ)
-  return M.path ~= nil and (M.config.types[typ] or M.config.types.all)
+  return file_path ~= nil and (types[typ] or types.all)
 end
 
 function M.setup(opts)
-  M.config = opts.log
-  if M.config and M.config.enable and M.config.types then
-    M.path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER)
-    if M.config.truncate then
-      os.remove(M.path)
+  if opts.log and opts.log.enable and opts.log.types then
+    types = opts.log.types
+    file_path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER)
+    if opts.log.truncate then
+      os.remove(file_path)
     end
-    require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. M.path)
+    require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. file_path)
   end
 end
 

From 0acbc7990934b59263010425f0adf7e4b81e1e82 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 3 Nov 2024 10:18:44 +1100
Subject: [PATCH 85/88] tidy hl icon name

---
 lua/nvim-tree.lua                           |  3 ---
 lua/nvim-tree/node/directory-link.lua       |  6 ++---
 lua/nvim-tree/node/directory.lua            |  2 +-
 lua/nvim-tree/node/file-link.lua            | 21 ++--------------
 lua/nvim-tree/node/file.lua                 |  9 ++++++-
 lua/nvim-tree/node/init.lua                 | 16 +++++++++++-
 lua/nvim-tree/renderer/components/icons.lua | 27 +++++++++++++++++----
 lua/nvim-tree/renderer/components/init.lua  |  2 +-
 8 files changed, 51 insertions(+), 35 deletions(-)

diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua
index 4390adcf35b..0ece7f1916e 100644
--- a/lua/nvim-tree.lua
+++ b/lua/nvim-tree.lua
@@ -736,9 +736,6 @@ function M.setup(conf)
   require("nvim-tree.buffers").setup(opts)
   require("nvim-tree.help").setup(opts)
   require("nvim-tree.watcher").setup(opts)
-  if M.config.renderer.icons.show.file and pcall(require, "nvim-web-devicons") then
-    require("nvim-web-devicons").setup()
-  end
 
   setup_autocommands(opts)
 
diff --git a/lua/nvim-tree/node/directory-link.lua b/lua/nvim-tree/node/directory-link.lua
index 8f9138d71ce..8cfa3760a9e 100644
--- a/lua/nvim-tree/node/directory-link.lua
+++ b/lua/nvim-tree/node/directory-link.lua
@@ -1,9 +1,7 @@
 local git_utils = require("nvim-tree.git.utils")
-local icons = require("nvim-tree.renderer.components.icons")
 local utils = require("nvim-tree.utils")
 
 local DirectoryNode = require("nvim-tree.node.directory")
-local Node = require("nvim-tree.node")
 
 ---@class (exact) DirectoryLinkNode: DirectoryNode
 ---@field link_to string absolute path
@@ -49,7 +47,7 @@ end
 ---@return HighlightedString name
 function DirectoryLinkNode:highlighted_icon()
   if not self.explorer.opts.renderer.icons.show.folder then
-    return Node.highlighted_icon(self)
+    return self:highlighted_icon_empty()
   end
 
   local str, hl
@@ -72,7 +70,7 @@ function DirectoryLinkNode:highlighted_name()
 
   if self.explorer.opts.renderer.symlink_destination then
     local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
-    name.str = string.format("%s%s%s", name.str, icons.i.symlink_arrow, link_to)
+    name.str = string.format("%s%s%s", name.str, self.explorer.opts.renderer.icons.symlink_arrow, link_to)
     name.hl = { "NvimTreeSymlinkFolderName" }
   end
 
diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index b6fe89ae7c6..9e8c8cdc52a 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -211,7 +211,7 @@ end
 ---@return HighlightedString icon
 function DirectoryNode:highlighted_icon()
   if not self.explorer.opts.renderer.icons.show.folder then
-    return Node.highlighted_icon(self)
+    return self:highlighted_icon_empty()
   end
 
   local str, hl
diff --git a/lua/nvim-tree/node/file-link.lua b/lua/nvim-tree/node/file-link.lua
index 095c4cc5504..0c168a50a93 100644
--- a/lua/nvim-tree/node/file-link.lua
+++ b/lua/nvim-tree/node/file-link.lua
@@ -1,9 +1,7 @@
 local git_utils = require("nvim-tree.git.utils")
-local icons = require("nvim-tree.renderer.components.icons")
 local utils = require("nvim-tree.utils")
 
 local FileNode = require("nvim-tree.node.file")
-local Node = require("nvim-tree.node")
 
 ---@class (exact) FileLinkNode: FileNode
 ---@field link_to string absolute path
@@ -44,8 +42,8 @@ end
 
 ---@return HighlightedString icon
 function FileLinkNode:highlighted_icon()
-  if not self.explorer.opts.renderer.icons.show.folder then
-    return Node.highlighted_icon(self)
+  if not self.explorer.opts.renderer.icons.show.file then
+    return self:highlighted_icon_empty()
   end
 
   local str, hl
@@ -68,21 +66,6 @@ function FileLinkNode:highlighted_name()
   return { str = str, hl = { "NvimTreeSymlink" } }
 end
 
----Icon and name for the file link
----@return HighlightedString icon
----@return HighlightedString name
-function FileLinkNode:icon_name()
-  local icon_str = icons.i.symlink
-
-  local name_str = self.name
-  if self.explorer.opts.renderer.symlink_destination then
-    local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
-    name_str = string.format("%s%s%s", name_str, icons.i.symlink_arrow, link_to)
-  end
-
-  return { str = icon_str, hl = { "NvimTreeSymlinkIcon" } }, { str = name_str, hl = { "NvimTreeSymlink" } }
-end
-
 ---Create a sanitized partial copy of a node
 ---@return FileLinkNode cloned
 function FileLinkNode:clone()
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 3333471921d..9b326bf89c9 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -69,7 +69,7 @@ end
 ---@return HighlightedString icon
 function FileNode:highlighted_icon()
   if not self.explorer.opts.renderer.icons.show.file then
-    return Node.highlighted_icon(self)
+    return self:highlighted_icon_empty()
   end
 
   local str, hl
@@ -77,6 +77,13 @@ function FileNode:highlighted_icon()
   -- devicon if enabled and available
   if self.explorer.opts.renderer.icons.web_devicons.file.enable then
     str, hl = icons.get_icon(self.name)
+    if not str then
+      local default_icon = icons.get_default_icon()
+      if default_icon then
+        str = default_icon.icon
+        hl = "DevIcon" .. default_icon.name
+      end
+    end
     if not self.explorer.opts.renderer.icons.web_devicons.file.color then
       hl = nil
     end
diff --git a/lua/nvim-tree/node/init.lua b/lua/nvim-tree/node/init.lua
index 3f8ff9cab04..48bf783c421 100644
--- a/lua/nvim-tree/node/init.lua
+++ b/lua/nvim-tree/node/init.lua
@@ -68,10 +68,24 @@ function Node:get_parent_of_group()
   end
 end
 
+---Empty highlighted icon
+---@protected
+---@return HighlightedString icon
+function Node:highlighted_icon_empty()
+  return { str = "", hl = {} }
+end
+
 ---Highlighted icon for the node
 ---Empty for base Node
 ---@return HighlightedString icon
 function Node:highlighted_icon()
+  return self:highlighted_icon_empty()
+end
+
+---Empty highlighted name
+---@protected
+---@return HighlightedString name
+function Node:highlighted_name_empty()
   return { str = "", hl = {} }
 end
 
@@ -79,7 +93,7 @@ end
 ---Empty for base Node
 ---@return HighlightedString icon
 function Node:highlighted_name()
-  return { str = "", hl = {} }
+  return self:highlighted_name_empty()
 end
 
 ---Create a sanitized partial copy of a node, populating children recursively.
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 54a8c4fb9a2..7c72a2314a1 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -13,7 +13,7 @@ local M = {
   devicons = nil,
 }
 
----Wrapper around nvim-web-devicons, nils if not present
+---Wrapper around nvim-web-devicons, nils if devicons not available
 ---@param name string
 ---@return string? icon
 ---@return string? hl_group
@@ -25,10 +25,27 @@ function M.get_icon(name)
   end
 end
 
-function M.setup()
-  local devicons_ok, devicons = pcall(require, "nvim-web-devicons")
-  if devicons_ok then
-    M.devicons = devicons
+---Wrapper around nvim-web-devicons, nil if devicons not available
+---@return DevIcon?
+function M.get_default_icon()
+  if M.devicons then
+    return M.devicons.get_default_icon()
+  else
+    return nil
+  end
+end
+
+---Attempt to use nvim-web-devicons if present and enabled for file or folder
+---@param opts table
+function M.setup(opts)
+  if opts.renderer.icons.show.file or opts.renderer.icons.show.folder then
+    local devicons_ok, devicons = pcall(require, "nvim-web-devicons")
+    if devicons_ok then
+      M.devicons = devicons
+
+      -- does nothing if already called i.e. don't clobber previous user setup
+      M.devicons.setup()
+    end
   end
 end
 
diff --git a/lua/nvim-tree/renderer/components/init.lua b/lua/nvim-tree/renderer/components/init.lua
index ff4ceafdcd3..dfb767e63d8 100644
--- a/lua/nvim-tree/renderer/components/init.lua
+++ b/lua/nvim-tree/renderer/components/init.lua
@@ -8,7 +8,7 @@ M.padding = require("nvim-tree.renderer.components.padding")
 function M.setup(opts)
   M.diagnostics.setup(opts)
   M.full_name.setup(opts)
-  M.icons.setup()
+  M.icons.setup(opts)
   M.padding.setup(opts)
 end
 

From 68183069e752b328d4b9f7dad96b8dfde4e0a7c9 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 3 Nov 2024 11:51:37 +1100
Subject: [PATCH 86/88] file devicon uses library to fall back

---
 lua/nvim-tree/node/file.lua                 | 11 +----
 lua/nvim-tree/renderer/components/icons.lua | 47 +++++++--------------
 2 files changed, 17 insertions(+), 41 deletions(-)

diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 9b326bf89c9..50883b086ce 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -74,16 +74,9 @@ function FileNode:highlighted_icon()
 
   local str, hl
 
-  -- devicon if enabled and available
+  -- devicon if enabled and available, fallback to default
   if self.explorer.opts.renderer.icons.web_devicons.file.enable then
-    str, hl = icons.get_icon(self.name)
-    if not str then
-      local default_icon = icons.get_default_icon()
-      if default_icon then
-        str = default_icon.icon
-        hl = "DevIcon" .. default_icon.name
-      end
-    end
+    str, hl = icons.get_icon(self.name, nil, { default = true })
     if not self.explorer.opts.renderer.icons.web_devicons.file.color then
       hl = nil
     end
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index 7c72a2314a1..ce1fc920d0f 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -1,50 +1,33 @@
----@class DevIcon
----@field icon string
----@field color string
----@field cterm_color string
----@field name string
+---@alias devicons_get_icon fun(name: string, ext: string?, opts: table?): string?, string?
+---@alias devicons_setup fun(opts: table?)
 
----@class DevIcons
----@field get_icon fun(name: string, ext: string?): string?, string?
----@field get_default_icon fun(): DevIcon
+---@class DevIcons?
+---@field setup devicons_setup
+---@field get_icon devicons_get_icon
+local devicons
 
-local M = {
-  ---@type DevIcons?
-  devicons = nil,
-}
+local M = {}
 
 ---Wrapper around nvim-web-devicons, nils if devicons not available
----@param name string
----@return string? icon
----@return string? hl_group
-function M.get_icon(name)
-  if M.devicons then
-    return M.devicons.get_icon(name, nil)
+---@type devicons_get_icon
+function M.get_icon(name, ext, opts)
+  if devicons then
+    return devicons.get_icon(name, ext, opts)
   else
     return nil, nil
   end
 end
 
----Wrapper around nvim-web-devicons, nil if devicons not available
----@return DevIcon?
-function M.get_default_icon()
-  if M.devicons then
-    return M.devicons.get_default_icon()
-  else
-    return nil
-  end
-end
-
 ---Attempt to use nvim-web-devicons if present and enabled for file or folder
 ---@param opts table
 function M.setup(opts)
   if opts.renderer.icons.show.file or opts.renderer.icons.show.folder then
-    local devicons_ok, devicons = pcall(require, "nvim-web-devicons")
-    if devicons_ok then
-      M.devicons = devicons
+    local ok, di = pcall(require, "nvim-web-devicons")
+    if ok then
+      devicons = di
 
       -- does nothing if already called i.e. don't clobber previous user setup
-      M.devicons.setup()
+      devicons.setup()
     end
   end
 end

From ea69ab3fdb75198d2180b4a328f579b97e9e40d2 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 3 Nov 2024 11:59:46 +1100
Subject: [PATCH 87/88] file devicon uses library to fall back

---
 lua/nvim-tree/renderer/components/icons.lua | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua
index ce1fc920d0f..ad91d058c15 100644
--- a/lua/nvim-tree/renderer/components/icons.lua
+++ b/lua/nvim-tree/renderer/components/icons.lua
@@ -1,7 +1,7 @@
 ---@alias devicons_get_icon fun(name: string, ext: string?, opts: table?): string?, string?
 ---@alias devicons_setup fun(opts: table?)
 
----@class DevIcons?
+---@class (strict) DevIcons?
 ---@field setup devicons_setup
 ---@field get_icon devicons_get_icon
 local devicons
@@ -24,9 +24,9 @@ function M.setup(opts)
   if opts.renderer.icons.show.file or opts.renderer.icons.show.folder then
     local ok, di = pcall(require, "nvim-web-devicons")
     if ok then
-      devicons = di
+      devicons = di --[[@as DevIcons]]
 
-      -- does nothing if already called i.e. don't clobber previous user setup
+      -- does nothing if already called i.e. doesn't clobber previous user setup
       devicons.setup()
     end
   end

From 112b1b63a47d6e34ed4c1949c621daaf5f9fdef4 Mon Sep 17 00:00:00 2001
From: Alexander Courtis <alex@courtis.org>
Date: Sun, 3 Nov 2024 13:03:29 +1100
Subject: [PATCH 88/88] file devicon uses library to fall back

---
 lua/nvim-tree/node/directory.lua                              | 2 +-
 lua/nvim-tree/node/file.lua                                   | 2 +-
 lua/nvim-tree/renderer/components/{icons.lua => devicons.lua} | 0
 lua/nvim-tree/renderer/components/init.lua                    | 4 ++--
 4 files changed, 4 insertions(+), 4 deletions(-)
 rename lua/nvim-tree/renderer/components/{icons.lua => devicons.lua} (100%)

diff --git a/lua/nvim-tree/node/directory.lua b/lua/nvim-tree/node/directory.lua
index 9e8c8cdc52a..3d87465aa04 100644
--- a/lua/nvim-tree/node/directory.lua
+++ b/lua/nvim-tree/node/directory.lua
@@ -1,5 +1,5 @@
 local git_utils = require("nvim-tree.git.utils")
-local icons = require("nvim-tree.renderer.components.icons")
+local icons = require("nvim-tree.renderer.components.devicons")
 local notify = require("nvim-tree.notify")
 local Node = require("nvim-tree.node")
 
diff --git a/lua/nvim-tree/node/file.lua b/lua/nvim-tree/node/file.lua
index 50883b086ce..18555fef26c 100644
--- a/lua/nvim-tree/node/file.lua
+++ b/lua/nvim-tree/node/file.lua
@@ -1,5 +1,5 @@
 local git_utils = require("nvim-tree.git.utils")
-local icons = require("nvim-tree.renderer.components.icons")
+local icons = require("nvim-tree.renderer.components.devicons")
 local utils = require("nvim-tree.utils")
 
 local Node = require("nvim-tree.node")
diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/devicons.lua
similarity index 100%
rename from lua/nvim-tree/renderer/components/icons.lua
rename to lua/nvim-tree/renderer/components/devicons.lua
diff --git a/lua/nvim-tree/renderer/components/init.lua b/lua/nvim-tree/renderer/components/init.lua
index dfb767e63d8..776350b4b77 100644
--- a/lua/nvim-tree/renderer/components/init.lua
+++ b/lua/nvim-tree/renderer/components/init.lua
@@ -2,13 +2,13 @@ local M = {}
 
 M.diagnostics = require("nvim-tree.renderer.components.diagnostics")
 M.full_name = require("nvim-tree.renderer.components.full-name")
-M.icons = require("nvim-tree.renderer.components.icons")
+M.devicons = require("nvim-tree.renderer.components.devicons")
 M.padding = require("nvim-tree.renderer.components.padding")
 
 function M.setup(opts)
   M.diagnostics.setup(opts)
   M.full_name.setup(opts)
-  M.icons.setup(opts)
+  M.devicons.setup(opts)
   M.padding.setup(opts)
 end