chore: resolve undefined-field warnings, fix link git statuses, rewrite devicons (#2968)

* add todo

* 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

* safer git_status_dir

* refactor(#2886): multi instance: node class refactoring: DirectoryNode:expand_or_collapse (#2957)

move expand_or_collapse to DirectoryNode

* 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

* move lib.get_cursor_position to Explorer

* move lib.get_node_at_cursor to Explorer

* move lib.get_nodes to Explorer

* move place_cursor_on_node to Explorer

* resolve resource leak in purge_all_state

* move many autocommands into Explorer

* post merge tidy

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit be546ff18d41f28466b065c857e1e041659bd2c8.

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit e82db1c44d.

* chore: resolve undefined-field

* chore: class new is now generic

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit 0e9b844d22.

* move icon builders into node classes

* move icon builders into node classes

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move file specifics from icons to File

* clean up sorters

* chore: resolve undefined-field

* tidy hl icon name

* file devicon uses library to fall back

* file devicon uses library to fall back

* file devicon uses library to fall back
This commit is contained in:
Alexander Courtis 2024-11-03 14:06:12 +11:00 committed by GitHub
parent c22124b374
commit 610a1c189b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1073 additions and 887 deletions

View File

@ -736,9 +736,6 @@ function M.setup(conf)
require("nvim-tree.buffers").setup(opts) require("nvim-tree.buffers").setup(opts)
require("nvim-tree.help").setup(opts) require("nvim-tree.help").setup(opts)
require("nvim-tree.watcher").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) setup_autocommands(opts)

View File

@ -9,31 +9,34 @@ local find_file = require("nvim-tree.actions.finders.find-file").fn
local DirectoryNode = require("nvim-tree.node.directory") local DirectoryNode = require("nvim-tree.node.directory")
---@enum ACTION ---@alias ClipboardAction "copy" | "cut"
local ACTION = { ---@alias ClipboardData table<ClipboardAction, Node[]>
copy = "copy",
cut = "cut", ---@alias ClipboardActionFn fun(source: string, dest: string): boolean, string?
}
---@class Clipboard to handle all actions.fs clipboard API ---@class Clipboard to handle all actions.fs clipboard API
---@field config table hydrated user opts.filters ---@field config table hydrated user opts.filters
---@field private explorer Explorer ---@field private explorer Explorer
---@field private data table<ACTION, Node[]> ---@field private data ClipboardData
---@field private clipboard_name string
---@field private reg string
local Clipboard = {} local Clipboard = {}
---@param opts table user options ---@param opts table user options
---@param explorer Explorer ---@param explorer Explorer
---@return Clipboard ---@return Clipboard
function Clipboard:new(opts, explorer) function Clipboard:new(opts, explorer)
---@type Clipboard
local o = { local o = {
explorer = explorer, explorer = explorer,
data = { data = {
[ACTION.copy] = {}, copy = {},
[ACTION.cut] = {}, cut = {},
}, },
clipboard_name = opts.actions.use_system_clipboard and "system" or "neovim",
reg = opts.actions.use_system_clipboard and "+" or "1",
config = { config = {
filesystem_watchers = opts.filesystem_watchers, filesystem_watchers = opts.filesystem_watchers,
actions = opts.actions,
}, },
} }
@ -47,13 +50,11 @@ end
---@return boolean ---@return boolean
---@return string|nil ---@return string|nil
local function do_copy(source, destination) local function do_copy(source, destination)
local source_stats, handle local source_stats, err = vim.loop.fs_stat(source)
local success, errmsg
source_stats, errmsg = vim.loop.fs_stat(source)
if not source_stats then if not source_stats then
log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg) log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, err)
return false, errmsg return false, err
end end
log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination) log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination)
@ -64,25 +65,28 @@ local function do_copy(source, destination)
end end
if source_stats.type == "file" then if source_stats.type == "file" then
success, errmsg = vim.loop.fs_copyfile(source, destination) local success
success, err = vim.loop.fs_copyfile(source, destination)
if not success then if not success then
log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg) log.line("copy_paste", "do_copy fs_copyfile failed '%s'", err)
return false, errmsg return false, err
end end
return true return true
elseif source_stats.type == "directory" then elseif source_stats.type == "directory" then
handle, errmsg = vim.loop.fs_scandir(source) local handle
handle, err = vim.loop.fs_scandir(source)
if type(handle) == "string" then if type(handle) == "string" then
return false, handle return false, handle
elseif not handle then elseif not handle then
log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, errmsg) log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, err)
return false, errmsg return false, err
end end
success, errmsg = vim.loop.fs_mkdir(destination, source_stats.mode) local success
success, err = vim.loop.fs_mkdir(destination, source_stats.mode)
if not success then if not success then
log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg) log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, err)
return false, errmsg return false, err
end end
while true do while true do
@ -93,15 +97,15 @@ local function do_copy(source, destination)
local new_name = utils.path_join({ source, name }) local new_name = utils.path_join({ source, name })
local new_destination = utils.path_join({ destination, 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 if not success then
return false, errmsg return false, err
end end
end end
else else
errmsg = string.format("'%s' illegal file type '%s'", source, source_stats.type) err = string.format("'%s' illegal file type '%s'", source, source_stats.type)
log.line("copy_paste", "do_copy %s", errmsg) log.line("copy_paste", "do_copy %s", err)
return false, errmsg return false, err
end end
return true return true
@ -109,28 +113,26 @@ end
---@param source string ---@param source string
---@param dest string ---@param dest string
---@param action ACTION ---@param action ClipboardAction
---@param action_fn fun(source: string, dest: string) ---@param action_fn ClipboardActionFn
---@return boolean|nil -- success ---@return boolean|nil -- success
---@return string|nil -- error message ---@return string|nil -- error message
local function do_single_paste(source, dest, action, action_fn) local function do_single_paste(source, dest, action, action_fn)
local dest_stats
local success, errmsg, errcode
local notify_source = notify.render_path(source) local notify_source = notify.render_path(source)
log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest) log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest)
dest_stats, errmsg, errcode = vim.loop.fs_stat(dest) local dest_stats, err, err_name = vim.loop.fs_stat(dest)
if not dest_stats and errcode ~= "ENOENT" then if not dest_stats and err_name ~= "ENOENT" then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???")) notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (err or "???"))
return false, errmsg return false, err
end end
local function on_process() local function on_process()
success, errmsg = action_fn(source, dest) local success, error = action_fn(source, dest)
if not success then if not success then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???")) notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???"))
return false, errmsg return false, error
end end
find_file(utils.path_remove_trailing(dest)) find_file(utils.path_remove_trailing(dest))
@ -173,7 +175,7 @@ local function do_single_paste(source, dest, action, action_fn)
end end
---@param node Node ---@param node Node
---@param clip table ---@param clip ClipboardData
local function toggle(node, clip) local function toggle(node, clip)
if node.name == ".." then if node.name == ".." then
return return
@ -191,8 +193,8 @@ end
---Clear copied and cut ---Clear copied and cut
function Clipboard:clear_clipboard() function Clipboard:clear_clipboard()
self.data[ACTION.copy] = {} self.data.copy = {}
self.data[ACTION.cut] = {} self.data.cut = {}
notify.info("Clipboard has been emptied.") notify.info("Clipboard has been emptied.")
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
@ -200,29 +202,32 @@ end
---Copy one node ---Copy one node
---@param node Node ---@param node Node
function Clipboard:copy(node) function Clipboard:copy(node)
utils.array_remove(self.data[ACTION.cut], node) utils.array_remove(self.data.cut, node)
toggle(node, self.data[ACTION.copy]) toggle(node, self.data.copy)
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
---Cut one node ---Cut one node
---@param node Node ---@param node Node
function Clipboard:cut(node) function Clipboard:cut(node)
utils.array_remove(self.data[ACTION.copy], node) utils.array_remove(self.data.copy, node)
toggle(node, self.data[ACTION.cut]) toggle(node, self.data.cut)
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
---Paste cut or cop ---Paste cut or cop
---@private ---@private
---@param node Node ---@param node Node
---@param action ACTION ---@param action ClipboardAction
---@param action_fn fun(source: string, dest: string) ---@param action_fn ClipboardActionFn
function Clipboard:do_paste(node, action, action_fn) function Clipboard:do_paste(node, action, action_fn)
if node.name == ".." then if node.name == ".." then
node = self.explorer node = self.explorer
elseif node:is(DirectoryNode) then else
node = node:last_group_node() local dir = node:as(DirectoryNode)
if dir then
node = dir:last_group_node()
end
end end
local clip = self.data[action] local clip = self.data[action]
if #clip == 0 then if #clip == 0 then
@ -230,10 +235,10 @@ function Clipboard:do_paste(node, action, action_fn)
end end
local destination = node.absolute_path local destination = node.absolute_path
local stats, errmsg, errcode = vim.loop.fs_stat(destination) local stats, err, err_name = vim.loop.fs_stat(destination)
if not stats and errcode ~= "ENOENT" then if not stats and err_name ~= "ENOENT" then
log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, errmsg) log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err)
notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (errmsg or "???")) notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???"))
return return
end end
local is_dir = stats and stats.type == "directory" local is_dir = stats and stats.type == "directory"
@ -278,24 +283,24 @@ end
---Paste cut (if present) or copy (if present) ---Paste cut (if present) or copy (if present)
---@param node Node ---@param node Node
function Clipboard:paste(node) function Clipboard:paste(node)
if self.data[ACTION.cut][1] ~= nil then if self.data.cut[1] ~= nil then
self:do_paste(node, ACTION.cut, do_cut) self:do_paste(node, "cut", do_cut)
elseif self.data[ACTION.copy][1] ~= nil then elseif self.data.copy[1] ~= nil then
self:do_paste(node, ACTION.copy, do_copy) self:do_paste(node, "copy", do_copy)
end end
end end
function Clipboard:print_clipboard() function Clipboard:print_clipboard()
local content = {} local content = {}
if #self.data[ACTION.cut] > 0 then if #self.data.cut > 0 then
table.insert(content, "Cut") 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))) table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end end
end end
if #self.data[ACTION.copy] > 0 then if #self.data.copy > 0 then
table.insert(content, "Copy") 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))) table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end end
end end
@ -305,65 +310,45 @@ end
---@param content string ---@param content string
function Clipboard:copy_to_reg(content) 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 -- manually firing TextYankPost does not set vim.v.event
-- workaround: create a scratch buffer with the clipboard contents and send a yank command -- workaround: create a scratch buffer with the clipboard contents and send a yank command
local temp_buf = vim.api.nvim_create_buf(false, true) 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_set_text(temp_buf, 0, 0, 0, 0, { content })
vim.api.nvim_buf_call(temp_buf, function() vim.api.nvim_buf_call(temp_buf, function()
vim.cmd(string.format('normal! "%sy$', reg)) vim.cmd(string.format('normal! "%sy$', self.reg))
end) end)
vim.api.nvim_buf_delete(temp_buf, {}) 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 end
---@param node Node ---@param node Node
function Clipboard:copy_filename(node) function Clipboard:copy_filename(node)
local content
if node.name == ".." then if node.name == ".." then
-- root -- root
content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t") self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t"))
else else
-- node -- node
content = node.name self:copy_to_reg(node.name)
end end
self:copy_to_reg(content)
end end
---@param node Node ---@param node Node
function Clipboard:copy_basename(node) function Clipboard:copy_basename(node)
local content
if node.name == ".." then if node.name == ".." then
-- root -- 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 else
-- node -- node
content = vim.fn.fnamemodify(node.name, ":r") self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
end end
self:copy_to_reg(content)
end end
---@param node Node ---@param node Node
function Clipboard:copy_path(node) function Clipboard:copy_path(node)
local content
if node.name == ".." then if node.name == ".." then
-- root -- root
content = utils.path_add_trailing("") self:copy_to_reg(utils.path_add_trailing(""))
else else
-- node -- node
local absolute_path = node.absolute_path local absolute_path = node.absolute_path
@ -373,10 +358,12 @@ function Clipboard:copy_path(node)
end end
local relative_path = utils.path_relative(absolute_path, cwd) local relative_path = utils.path_relative(absolute_path, cwd)
content = node.nodes ~= nil 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 end
self:copy_to_reg(content)
end end
---@param node Node ---@param node Node
@ -394,14 +381,14 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
function Clipboard:is_cut(node) function Clipboard:is_cut(node)
return vim.tbl_contains(self.data[ACTION.cut], node) return vim.tbl_contains(self.data.cut, node)
end end
---Node is copied. Will not be cut. ---Node is copied. Will not be cut.
---@param node Node ---@param node Node
---@return boolean ---@return boolean
function Clipboard:is_copied(node) function Clipboard:is_copied(node)
return vim.tbl_contains(self.data[ACTION.copy], node) return vim.tbl_contains(self.data.copy, node)
end end
return Clipboard return Clipboard

View File

@ -34,7 +34,7 @@ end
---@param node Node? ---@param node Node?
function M.fn(node) function M.fn(node)
node = node or core.get_explorer() --[[@as Node]] node = node or core.get_explorer()
if not node then if not node then
return return
end end

View File

@ -5,6 +5,9 @@ local view = require("nvim-tree.view")
local lib = require("nvim-tree.lib") local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
@ -89,7 +92,7 @@ end
---@param node Node ---@param node Node
function M.remove(node) function M.remove(node)
local notify_node = notify.render_path(node.absolute_path) 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) local success = remove_dir(node.absolute_path)
if not success then if not success then
notify.error("Could not remove " .. notify_node) notify.error("Could not remove " .. notify_node)

View File

@ -125,8 +125,9 @@ function M.fn(default_modifier)
return return
end end
if node:is(DirectoryNode) then local dir = node:as(DirectoryNode)
node = node:last_group_node() if dir then
node = dir:last_group_node()
end end
if node.name == ".." then if node.name == ".." then
return return

View File

@ -2,6 +2,9 @@ local core = require("nvim-tree.core")
local lib = require("nvim-tree.lib") local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
@ -54,7 +57,7 @@ function M.remove(node)
local explorer = core.get_explorer() 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) trash_path(function(_, rc)
if rc ~= 0 then if rc ~= 0 then
notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash")

View File

@ -3,6 +3,7 @@ local view = require("nvim-tree.view")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local diagnostics = require("nvim-tree.diagnostics") local diagnostics = require("nvim-tree.diagnostics")
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory") local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
@ -10,14 +11,14 @@ local MAX_DEPTH = 100
---Return the status of the node or nil if no status, depending on the type of ---Return the status of the node or nil if no status, depending on the type of
---status. ---status.
---@param node table node to inspect ---@param node Node to inspect
---@param what string type of status ---@param what string? type of status
---@param skip_gitignored boolean default false ---@param skip_gitignored boolean? default false
---@return boolean ---@return boolean
local function status_is_valid(node, what, skip_gitignored) local function status_is_valid(node, what, skip_gitignored)
if what == "git" then if what == "git" then
local git_status = node:get_git_status() local git_xy = node:get_git_xy()
return git_status ~= nil and (not skip_gitignored or git_status[1] ~= "!!") return git_xy ~= nil and (not skip_gitignored or git_xy[1] ~= "!!")
elseif what == "diag" then elseif what == "diag" then
local diag_status = diagnostics.get_diag_status(node) local diag_status = diagnostics.get_diag_status(node)
return diag_status ~= nil and diag_status.value ~= nil return diag_status ~= nil and diag_status.value ~= nil
@ -30,9 +31,9 @@ end
---Move to the next node that has a valid status. If none found, don't move. ---Move to the next node that has a valid status. If none found, don't move.
---@param explorer Explorer ---@param explorer Explorer
---@param where string where to move (forwards or backwards) ---@param where string? where to move (forwards or backwards)
---@param what string type of status ---@param what string? type of status
---@param skip_gitignored boolean default false ---@param skip_gitignored boolean? default false
local function move(explorer, where, what, skip_gitignored) local function move(explorer, where, what, skip_gitignored)
local first_node_line = core.get_nodes_starting_line() local first_node_line = core.get_nodes_starting_line()
local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line) local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line)
@ -83,8 +84,8 @@ end
--- Move to the next node recursively. --- Move to the next node recursively.
---@param explorer Explorer ---@param explorer Explorer
---@param what string type of status ---@param what string? type of status
---@param skip_gitignored boolean default false ---@param skip_gitignored? boolean default false
local function move_next_recursive(explorer, what, skip_gitignored) local function move_next_recursive(explorer, what, skip_gitignored)
-- If the current node: -- If the current node:
-- * is a directory -- * is a directory
@ -149,8 +150,8 @@ end
--- 4.5) Save the current node and start back from 4.1. --- 4.5) Save the current node and start back from 4.1.
--- ---
---@param explorer Explorer ---@param explorer Explorer
---@param what string type of status ---@param what string? type of status
---@param skip_gitignored boolean default false ---@param skip_gitignored boolean? default false
local function move_prev_recursive(explorer, what, skip_gitignored) local function move_prev_recursive(explorer, what, skip_gitignored)
local node_init, node_cur local node_init, node_cur
@ -175,7 +176,7 @@ local function move_prev_recursive(explorer, what, skip_gitignored)
if if
node_cur == nil node_cur == nil
or node_cur == node_init -- we didn't move 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 then
return return
end end
@ -209,8 +210,10 @@ local function move_prev_recursive(explorer, what, skip_gitignored)
end end
---@class NavigationItemOpts ---@class NavigationItemOpts
---@field where string ---@field where string?
---@field what string ---@field what string?
---@field skip_gitignored boolean?
---@field recurse boolean?
---@param opts NavigationItemOpts ---@param opts NavigationItemOpts
---@return fun() ---@return fun()
@ -222,26 +225,21 @@ function M.fn(opts)
end end
local recurse = false local recurse = false
local skip_gitignored = false
-- recurse only valid for git and diag moves. -- recurse only valid for git and diag moves.
if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then
recurse = opts.recurse recurse = opts.recurse
end end
if opts.skip_gitignored ~= nil then
skip_gitignored = opts.skip_gitignored
end
if not recurse then if not recurse then
move(explorer, opts.where, opts.what, skip_gitignored) move(explorer, opts.where, opts.what, opts.skip_gitignored)
return return
end end
if opts.where == "next" then 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 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 end
end end

View File

@ -9,6 +9,7 @@ local keymap = require("nvim-tree.keymap")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryNode = require("nvim-tree.node.directory") local DirectoryNode = require("nvim-tree.node.directory")
local FileLinkNode = require("nvim-tree.node.file-link")
local RootNode = require("nvim-tree.node.root") local RootNode = require("nvim-tree.node.root")
local Api = { local Api = {
@ -140,8 +141,11 @@ end)
Api.tree.change_root_to_node = wrap_node(function(node) Api.tree.change_root_to_node = wrap_node(function(node)
if node.name == ".." or node:is(RootNode) then if node.name == ".." or node:is(RootNode) then
actions.root.change_dir.fn("..") actions.root.change_dir.fn("..")
elseif node:is(DirectoryNode) then else
actions.root.change_dir.fn(node:last_group_node().absolute_path) node = node:as(DirectoryNode)
if node then
actions.root.change_dir.fn(node:last_group_node().absolute_path)
end
end end
end) end)
@ -203,10 +207,8 @@ Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_pa
---@param mode string ---@param mode string
---@param node Node ---@param node Node
local function edit(mode, node) local function edit(mode, node)
local path = node.absolute_path local file_link = node:as(FileLinkNode)
if node.link_to and not node.nodes then local path = file_link and file_link.link_to or node.absolute_path
path = node.link_to
end
actions.node.open_file.fn(mode, path) actions.node.open_file.fn(mode, path)
end end
@ -216,10 +218,13 @@ end
local function open_or_expand_or_dir_up(mode, toggle_group) local function open_or_expand_or_dir_up(mode, toggle_group)
---@param node Node ---@param node Node
return function(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("..") actions.root.change_dir.fn("..")
elseif node:is(DirectoryNode) then elseif dir then
node:expand_or_collapse(toggle_group) dir:expand_or_collapse(toggle_group)
elseif not toggle_group then elseif not toggle_group then
edit(mode, node) edit(mode, node)
end end

View File

@ -1,3 +1,5 @@
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---@type table<string, boolean> record of which file is modified ---@type table<string, boolean> record of which file is modified
@ -24,11 +26,26 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
function M.is_modified(node) function M.is_modified(node)
return node if not M.config.modified.enable then
and M.config.modified.enable return false
and M._modified[node.absolute_path] end
and (not node.nodes or M.config.modified.show_on_dirs)
and (not node.open or M.config.modified.show_on_open_dirs) if not M._modified[node.absolute_path] then
return false
end
local dir = node:as(DirectoryNode)
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 end
---A buffer exists for the node's absolute path ---A buffer exists for the node's absolute path

View File

@ -1,22 +1,24 @@
---Generic class, useful for inheritence. ---Generic class, useful for inheritence.
---@class (exact) Class ---@class (exact) Class
---@field private __index? table
local Class = {} local Class = {}
---@param o Class? ---@generic T
---@return Class ---@param self T
---@param o T|nil
---@return T
function Class:new(o) function Class:new(o)
o = o or {} o = o or {}
setmetatable(o, self) setmetatable(o, self)
self.__index = self self.__index = self ---@diagnostic disable-line: inject-field
return o return o
end end
---Object is an instance of class ---Object is an instance of class
---This will start with the lowest class and loop over all the superclasses. ---This will start with the lowest class and loop over all the superclasses.
---@param class table ---@generic T
---@param class T
---@return boolean ---@return boolean
function Class:is(class) function Class:is(class)
local mt = getmetatable(self) local mt = getmetatable(self)
@ -32,9 +34,14 @@ end
---Return object if it is an instance of class, otherwise nil ---Return object if it is an instance of class, otherwise nil
---@generic T ---@generic T
---@param class T ---@param class T
---@return `T`|nil ---@return T|nil
function Class:as(class) function Class:as(class)
return self:is(class) and self or nil return self:is(class) and self or nil
end end
-- avoid unused param warnings in abstract methods
---@param ... any
function Class:nop(...) --luacheck: ignore 212
end
return Class return Class

View File

@ -3,6 +3,8 @@ local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---COC severity level strings to LSP severity levels ---COC severity level strings to LSP severity levels
@ -125,7 +127,7 @@ end
local function from_cache(node) local function from_cache(node)
local nodepath = uniformize_path(node.absolute_path) local nodepath = uniformize_path(node.absolute_path)
local max_severity = nil local max_severity = nil
if not node.nodes then if not node:is(DirectoryNode) then
-- direct cache hit for files -- direct cache hit for files
max_severity = NODE_SEVERITIES[nodepath] max_severity = NODE_SEVERITIES[nodepath]
else else
@ -190,7 +192,7 @@ function M.get_diag_status(node)
end end
-- dir but we shouldn't show on dirs at all -- 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 return nil
end end
@ -201,13 +203,15 @@ function M.get_diag_status(node)
node.diag_status = from_cache(node) node.diag_status = from_cache(node)
end end
local dir = node:as(DirectoryNode)
-- file -- file
if not node.nodes then if not dir then
return node.diag_status return node.diag_status
end end
-- dir is closed or we should show on open_dirs -- 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 return node.diag_status
end end
return nil return nil

View File

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

View File

@ -59,7 +59,7 @@ function Explorer:create(path)
local o = RootNode:create(explorer_placeholder, path, "..", nil) local o = RootNode:create(explorer_placeholder, path, "..", nil)
o = self:new(o) --[[@as Explorer]] o = self:new(o)
o.explorer = o o.explorer = o
@ -69,7 +69,7 @@ function Explorer:create(path)
o.open = true o.open = true
o.opts = config o.opts = config
o.sorters = Sorters:new(config) o.sorters = Sorters:create(config)
o.renderer = Renderer:new(config, o) o.renderer = Renderer:new(config, o)
o.filters = Filters:new(config, o) o.filters = Filters:new(config, o)
o.live_filter = LiveFilter:new(config, o) o.live_filter = LiveFilter:new(config, o)
@ -187,9 +187,9 @@ function Explorer:expand(node)
end end
---@param node DirectoryNode ---@param node DirectoryNode
---@param git_status table|nil ---@param project GitProject?
---@return Node[]? ---@return Node[]?
function Explorer:reload(node, git_status) function Explorer:reload(node, project)
local cwd = node.link_to or node.absolute_path local cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd) local handle = vim.loop.fs_scandir(cwd)
if not handle then 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 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 if node.group_next then
node.nodes = { node.group_next } node.nodes = { node.group_next }
@ -268,7 +268,7 @@ function Explorer:reload(node, git_status)
end end
node.nodes = vim.tbl_map( 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) vim.tbl_filter(function(n)
if remain_childs[n.absolute_path] then if remain_childs[n.absolute_path] then
return remain_childs[n.absolute_path] return remain_childs[n.absolute_path]
@ -282,7 +282,7 @@ function Explorer:reload(node, git_status)
local single_child = node:single_child_directory() local single_child = node:single_child_directory()
if config.renderer.group_empty and node.parent and single_child then if config.renderer.group_empty and node.parent and single_child then
node.group_next = single_child node.group_next = single_child
local ns = self:reload(single_child, git_status) local ns = self:reload(single_child, project)
node.nodes = ns or {} node.nodes = ns or {}
log.profile_end(profile) log.profile_end(profile)
return ns return ns
@ -321,7 +321,7 @@ function Explorer:refresh_parent_nodes_for_path(path)
local project = git.get_project(toplevel) or {} local project = git.get_project(toplevel) or {}
self:reload(node, project) self:reload(node, project)
node:update_parent_statuses(project, toplevel) git.update_parent_projects(node, project, toplevel)
end end
log.profile_end(profile) log.profile_end(profile)
@ -331,19 +331,19 @@ end
---@param node DirectoryNode ---@param node DirectoryNode
function Explorer:_load(node) function Explorer:_load(node)
local cwd = node.link_to or node.absolute_path local cwd = node.link_to or node.absolute_path
local git_status = git.load_project_status(cwd) local project = git.load_project(cwd)
self:explore(node, git_status, self) self:explore(node, project, self)
end end
---@private ---@private
---@param nodes_by_path Node[] ---@param nodes_by_path Node[]
---@param node_ignored boolean ---@param node_ignored boolean
---@param status table|nil ---@param project GitProject?
---@return fun(node: Node): table ---@return fun(node: Node): Node
function Explorer:update_status(nodes_by_path, node_ignored, status) function Explorer:update_git_statuses(nodes_by_path, node_ignored, project)
return function(node) return function(node)
if nodes_by_path[node.absolute_path] then if nodes_by_path[node.absolute_path] then
node:update_git_status(node_ignored, status) node:update_git_status(node_ignored, project)
end end
return node return node
end end
@ -353,13 +353,13 @@ end
---@param handle uv.uv_fs_t ---@param handle uv.uv_fs_t
---@param cwd string ---@param cwd string
---@param node DirectoryNode ---@param node DirectoryNode
---@param git_status table ---@param project GitProject
---@param parent Explorer ---@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 node_ignored = node:is_git_ignored()
local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") 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 {}, { node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0, git = 0,
@ -388,7 +388,7 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent)
if child then if child then
table.insert(node.nodes, child) table.insert(node.nodes, child)
nodes_by_path[child.absolute_path] = true nodes_by_path[child.absolute_path] = true
child:update_git_status(node_ignored, git_status) child:update_git_status(node_ignored, project)
end end
else else
for reason, value in pairs(FILTER_REASON) do for reason, value in pairs(FILTER_REASON) do
@ -405,10 +405,10 @@ end
---@private ---@private
---@param node DirectoryNode ---@param node DirectoryNode
---@param status table ---@param project GitProject
---@param parent Explorer ---@param parent Explorer
---@return Node[]|nil ---@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 cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd) local handle = vim.loop.fs_scandir(cwd)
if not handle then if not handle then
@ -417,15 +417,15 @@ function Explorer:explore(node, status, parent)
local profile = log.profile_start("explore %s", node.absolute_path) 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 is_root = not node.parent
local single_child = node:single_child_directory() local single_child = node:single_child_directory()
if config.renderer.group_empty and not is_root and single_child then 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_cwd = single_child.link_to or single_child.absolute_path
local child_status = git.load_project_status(child_cwd) local child_project = git.load_project(child_cwd)
node.group_next = single_child 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 {} node.nodes = ns or {}
log.profile_end(profile) log.profile_end(profile)
@ -440,7 +440,7 @@ function Explorer:explore(node, status, parent)
end end
---@private ---@private
---@param projects table ---@param projects GitProject[]
function Explorer:refresh_nodes(projects) function Explorer:refresh_nodes(projects)
Iterator.builder({ self }) Iterator.builder({ self })
:applier(function(n) :applier(function(n)
@ -463,7 +463,7 @@ function Explorer:reload_explorer()
end end
event_running = true event_running = true
local projects = git.reload() local projects = git.reload_all_projects()
self:refresh_nodes(projects) self:refresh_nodes(projects)
if view.is_visible() then if view.is_visible() then
self.renderer:draw() self.renderer:draw()
@ -477,8 +477,8 @@ function Explorer:reload_git()
end end
event_running = true event_running = true
local projects = git.reload() local projects = git.reload_all_projects()
self:reload_node_status(projects) git.reload_node_status(self, projects)
self.renderer:draw() self.renderer:draw()
event_running = false event_running = false
end end

View File

@ -1,16 +1,32 @@
local Class = require("nvim-tree.class")
local DirectoryNode = require("nvim-tree.node.directory")
local C = {} local C = {}
---@class Sorter ---@class (exact) SorterCfg
local Sorter = {} ---@field sorter string|fun(nodes: Node[])
---@field folders_first boolean
---@field files_first boolean
function Sorter:new(opts) ---@class (exact) Sorter: Class
local o = {} ---@field cfg SorterCfg
setmetatable(o, self) ---@field user fun(nodes: Node[])?
self.__index = self ---@field pre string?
o.config = vim.deepcopy(opts.sort) local Sorter = Class:new()
if type(o.config.sorter) == "function" then ---@param opts table user options
o.user = o.config.sorter ---@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 end
return o return o
end end
@ -20,7 +36,7 @@ end
---@return fun(a: Node, b: Node): boolean ---@return fun(a: Node, b: Node): boolean
function Sorter:get_comparator(sorter) function Sorter:get_comparator(sorter)
return function(a, b) 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
end end
@ -41,17 +57,17 @@ end
---Evaluate `sort.folders_first` and `sort.files_first` ---Evaluate `sort.folders_first` and `sort.files_first`
---@param a Node ---@param a Node
---@param b Node ---@param b Node
---@param cfg table ---@param cfg SorterCfg
---@return boolean|nil ---@return boolean|nil
local function folders_or_files_first(a, b, cfg) local function folders_or_files_first(a, b, cfg)
if not (cfg.folders_first or cfg.files_first) then if not (cfg.folders_first or cfg.files_first) then
return return
end end
if not a.nodes and b.nodes then if not a:is(DirectoryNode) and b:is(DirectoryNode) then
-- file <> folder -- file <> folder
return cfg.files_first return cfg.files_first
elseif a.nodes and not b.nodes then elseif a:is(DirectoryNode) and not b:is(DirectoryNode) then
-- folder <> file -- folder <> file
return not cfg.files_first return not cfg.files_first
end end
@ -157,15 +173,15 @@ function Sorter:sort(t)
end end
split_merge(t, 1, #t, mini_comparator) -- sort by user order split_merge(t, 1, #t, mini_comparator) -- sort by user order
else elseif self.pre then
split_merge(t, 1, #t, self:get_comparator(self.config.sorter)) split_merge(t, 1, #t, self:get_comparator(self.pre))
end end
end end
---@param a Node ---@param a Node
---@param b Node ---@param b Node
---@param ignorecase boolean|nil ---@param ignorecase boolean|nil
---@param cfg table ---@param cfg SorterCfg
---@return boolean ---@return boolean
local function node_comparator_name_ignorecase_or_not(a, b, ignorecase, cfg) local function node_comparator_name_ignorecase_or_not(a, b, ignorecase, cfg)
if not (a and b) then if not (a and b) then

View File

@ -1,4 +1,5 @@
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local git = require("nvim-tree.git")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local Watcher = require("nvim-tree.watcher").Watcher local Watcher = require("nvim-tree.watcher").Watcher
@ -65,9 +66,10 @@ function M.create_watcher(node)
return nil return nil
end end
---@param watcher Watcher
local function callback(watcher) local function callback(watcher)
log.line("watcher", "node event scheduled refresh %s", watcher.context) log.line("watcher", "node event scheduled refresh %s", watcher.data.context)
utils.debounce(watcher.context, M.config.filesystem_watchers.debounce_delay, function() utils.debounce(watcher.data.context, M.config.filesystem_watchers.debounce_delay, function()
if watcher.destroyed then if watcher.destroyed then
return return
end end
@ -76,12 +78,12 @@ function M.create_watcher(node)
else else
log.line("watcher", "node event executing refresh '%s'", node.absolute_path) log.line("watcher", "node event executing refresh '%s'", node.absolute_path)
end end
node:refresh() git.refresh_dir(node)
end) end)
end end
M.uid = M.uid + 1 M.uid = M.uid + 1
return Watcher:new(path, nil, callback, { return Watcher:create(path, nil, callback, {
context = "explorer:watch:" .. path .. ":" .. M.uid, context = "explorer:watch:" .. path .. ":" .. M.uid,
}) })
end end

View File

@ -1,24 +1,48 @@
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local git_utils = require("nvim-tree.git.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 Watcher = require("nvim-tree.watcher").Watcher
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class GitStatus ---Git short format status xy
---@field file string|nil ---@alias GitXY string
---@field dir table|nil
-- Git short-format status
---@alias GitPathXY table<string, GitXY>
-- Git short-format statuses
---@alias GitPathXYs table<string, GitXY[]>
---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
---@field files GitProjectFiles?
---@field dirs GitProjectDirs?
---@field watcher Watcher?
---@alias GitProjectFiles GitPathXY
---@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs>
local M = { local M = {
config = {}, config = {},
-- all projects keyed by toplevel ---all projects keyed by toplevel
---@type table<string, GitProject>
_projects_by_toplevel = {}, _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 = {}, _toplevels_by_path = {},
-- git dirs by toplevel -- git dirs by toplevel
---@type table<string, string>
_git_dirs_by_toplevel = {}, _git_dirs_by_toplevel = {},
} }
@ -34,35 +58,35 @@ local WATCHED_FILES = {
---@param toplevel string|nil ---@param toplevel string|nil
---@param path string|nil ---@param path string|nil
---@param project table ---@param project GitProject
---@param git_status table|nil ---@param project_files GitProjectFiles?
local function reload_git_status(toplevel, path, project, git_status) local function reload_git_project(toplevel, path, project, project_files)
if path then if path then
for p in pairs(project.files) do for p in pairs(project.files) do
if p:find(path, 1, true) == 1 then if p:find(path, 1, true) == 1 then
project.files[p] = nil project.files[p] = nil
end end
end end
project.files = vim.tbl_deep_extend("force", project.files, git_status) project.files = vim.tbl_deep_extend("force", project.files, project_files)
else else
project.files = git_status project.files = project_files or {}
end 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 end
--- Is this path in a known ignored directory? --- Is this path in a known ignored directory?
---@param path string ---@param path string
---@param project table git status ---@param project GitProject
---@return boolean ---@return boolean
local function path_ignored_in_project(path, project) local function path_ignored_in_project(path, project)
if not path or not project then if not path or not project then
return false return false
end end
if project and project.files then if project.files then
for file, status in pairs(project.files) do for p, xy in pairs(project.files) do
if status == "!!" and vim.startswith(path, file) then if xy == "!!" and vim.startswith(path, p) then
return true return true
end end
end end
@ -70,9 +94,8 @@ local function path_ignored_in_project(path, project)
return false return false
end end
--- Reload all projects ---@return GitProject[] maybe empty
---@return table projects maybe empty function M.reload_all_projects()
function M.reload()
if not M.config.git.enable then if not M.config.git.enable then
return {} return {}
end end
@ -85,11 +108,12 @@ function M.reload()
end end
--- Reload one project. Does nothing when no project or path is ignored --- Reload one project. Does nothing when no project or path is ignored
---@param toplevel string|nil ---@param toplevel string?
---@param path string|nil optional path to update only ---@param path string? optional path to update only
---@param callback function|nil ---@param callback function?
function M.reload_project(toplevel, path, callback) 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 not toplevel or not project or not M.config.git.enable then
if callback then if callback then
callback() callback()
@ -104,7 +128,8 @@ function M.reload_project(toplevel, path, callback)
return return
end end
local opts = { ---@type GitRunnerOpts
local runner_opts = {
toplevel = toplevel, toplevel = toplevel,
path = path, path = path,
list_untracked = git_utils.should_show_untracked(toplevel), list_untracked = git_utils.should_show_untracked(toplevel),
@ -113,20 +138,21 @@ function M.reload_project(toplevel, path, callback)
} }
if callback then if callback then
Runner.run(opts, function(git_status) ---@param path_xy GitPathXY
reload_git_status(toplevel, path, project, git_status) runner_opts.callback = function(path_xy)
reload_git_project(toplevel, path, project, path_xy)
callback() callback()
end) end
GitRunner:run(runner_opts)
else else
-- TODO #1974 use callback once async/await is available -- TODO #1974 use callback once async/await is available
local git_status = Runner.run(opts) reload_git_project(toplevel, path, project, GitRunner:run(runner_opts))
reload_git_status(toplevel, path, project, git_status)
end end
end end
--- Retrieve a known project --- Retrieve a known project
---@param toplevel string|nil ---@param toplevel string?
---@return table|nil project ---@return GitProject? project
function M.get_project(toplevel) function M.get_project(toplevel)
return M._projects_by_toplevel[toplevel] return M._projects_by_toplevel[toplevel]
end end
@ -147,11 +173,10 @@ function M.get_toplevel(path)
return nil return nil
end end
if M._toplevels_by_path[path] then local tl = M._toplevels_by_path[path]
return M._toplevels_by_path[path] if tl then
end return tl
elseif tl == false then
if M._toplevels_by_path[path] == false then
return nil return nil
end end
@ -190,8 +215,15 @@ function M.get_toplevel(path)
end end
M._toplevels_by_path[path] = toplevel M._toplevels_by_path[path] = toplevel
M._git_dirs_by_toplevel[toplevel] = git_dir 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 end
local function reload_tree_at(toplevel) local function reload_tree_at(toplevel)
@ -206,13 +238,13 @@ local function reload_tree_at(toplevel)
end end
M.reload_project(toplevel, nil, function() M.reload_project(toplevel, nil, function()
local git_status = M.get_project(toplevel) local project = M.get_project(toplevel)
Iterator.builder(root_node.nodes) Iterator.builder(root_node.nodes)
:hidden() :hidden()
:applier(function(node) :applier(function(node)
local parent_ignored = node.parent and node.parent:is_git_ignored() or false 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) end)
:recursor(function(node) :recursor(function(node)
return node.nodes and #node.nodes > 0 and node.nodes return node.nodes and #node.nodes > 0 and node.nodes
@ -226,8 +258,8 @@ end
--- Load the project status for a path. Does nothing when no toplevel for path. --- Load the project status for a path. Does nothing when no toplevel for path.
--- Only fetches project status when unknown, otherwise returns existing. --- Only fetches project status when unknown, otherwise returns existing.
---@param path string absolute ---@param path string absolute
---@return table project maybe empty ---@return GitProject maybe empty
function M.load_project_status(path) function M.load_project(path)
if not M.config.git.enable then if not M.config.git.enable then
return {} return {}
end end
@ -238,12 +270,12 @@ function M.load_project_status(path)
return {} return {}
end end
local status = M._projects_by_toplevel[toplevel] local project = M._projects_by_toplevel[toplevel]
if status then if project then
return status return project
end end
local git_status = Runner.run({ local path_xys = GitRunner:run({
toplevel = toplevel, toplevel = toplevel,
list_untracked = git_utils.should_show_untracked(toplevel), list_untracked = git_utils.should_show_untracked(toplevel),
list_ignored = true, list_ignored = true,
@ -254,26 +286,27 @@ function M.load_project_status(path)
if M.config.filesystem_watchers.enable then if M.config.filesystem_watchers.enable then
log.line("watcher", "git start") log.line("watcher", "git start")
---@param w Watcher
local callback = function(w) local callback = function(w)
log.line("watcher", "git event scheduled '%s'", w.toplevel) log.line("watcher", "git event scheduled '%s'", w.data.toplevel)
utils.debounce("git:watcher:" .. w.toplevel, M.config.filesystem_watchers.debounce_delay, function() utils.debounce("git:watcher:" .. w.data.toplevel, M.config.filesystem_watchers.debounce_delay, function()
if w.destroyed then if w.destroyed then
return return
end end
reload_tree_at(w.toplevel) reload_tree_at(w.data.toplevel)
end) end)
end end
local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join({ toplevel, ".git" }) 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, toplevel = toplevel,
}) })
end end
if git_status then if path_xys then
M._projects_by_toplevel[toplevel] = { M._projects_by_toplevel[toplevel] = {
files = git_status, files = path_xys,
dirs = git_utils.file_status_to_dir_status(git_status, toplevel), dirs = git_utils.project_files_to_project_dirs(path_xys, toplevel),
watcher = watcher, watcher = watcher,
} }
return M._projects_by_toplevel[toplevel] return M._projects_by_toplevel[toplevel]
@ -283,46 +316,69 @@ function M.load_project_status(path)
end end
end end
---Git file and directory status for an absolute path with optional file fallback ---@param dir DirectoryNode
---@param parent_ignored boolean ---@param project GitProject?
---@param status table|nil ---@param root string?
---@param path string function M.update_parent_projects(dir, project, root)
---@param path_file string? alternative file path when no other file status while project and dir do
---@return GitStatus|nil -- step up to the containing project
function M.git_status_dir(parent_ignored, status, path, path_file) if dir.absolute_path == root then
if parent_ignored then -- stop at the top of the tree
return { file = "!!" } if not dir.parent then
end break
end
if status then root = M.get_toplevel(dir.parent.absolute_path)
return {
file = status.files and (status.files[path] or status.files[path_file]), -- stop when no more projects
dir = status.dirs and { if not root then
direct = status.dirs.direct and status.dirs.direct[path], break
indirect = status.dirs.indirect and status.dirs.indirect[path], 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
end end
---Git file status for an absolute path with optional fallback ---Refresh contents and git status for a single directory
---@param parent_ignored boolean ---@param dir DirectoryNode
---@param status table|nil function M.refresh_dir(dir)
---@param path string local node = dir:get_parent_of_group() or dir
---@param path_fallback string? local toplevel = M.get_toplevel(dir.absolute_path)
---@return GitStatus
function M.git_status_file(parent_ignored, status, path, path_fallback) M.reload_project(toplevel, dir.absolute_path, function()
if parent_ignored then local project = M.get_project(toplevel) or {}
return { file = "!!" }
dir.explorer:reload(node, project)
M.update_parent_projects(dir, project, toplevel)
dir.explorer.renderer:draw()
end)
end
---@param dir DirectoryNode?
---@param projects GitProject[]
function M.reload_node_status(dir, projects)
dir = dir and dir:as(DirectoryNode)
if not dir or #dir.nodes == 0 then
return
end end
if not status or not status.files then local toplevel = M.get_toplevel(dir.absolute_path)
return {} local project = projects[toplevel] or {}
for _, node in ipairs(dir.nodes) do
node:update_git_status(dir:is_git_ignored(), project)
M.reload_node_status(node:as(DirectoryNode), projects)
end end
return {
file = status.files[path] or status.files[path_fallback]
}
end end
function M.purge_state() function M.purge_state()

View File

@ -2,9 +2,21 @@ local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
---@class Runner local Class = require("nvim-tree.class")
local Runner = {}
Runner.__index = Runner ---@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(path_xy: GitPathXY)?
---@class (exact) GitRunner: Class
---@field private opts GitRunnerOpts
---@field private path_xy GitPathXY
---@field private rc integer? -- -1 indicates timeout
local GitRunner = Class:new()
local timeouts = 0 local timeouts = 0
local MAX_TIMEOUTS = 5 local MAX_TIMEOUTS = 5
@ -12,7 +24,7 @@ local MAX_TIMEOUTS = 5
---@private ---@private
---@param status string ---@param status string
---@param path string|nil ---@param path string|nil
function Runner:_parse_status_output(status, path) function GitRunner:parse_status_output(status, path)
if not path then if not path then
return return
end end
@ -22,7 +34,7 @@ function Runner:_parse_status_output(status, path)
path = path:gsub("/", "\\") path = path:gsub("/", "\\")
end end
if #status > 0 and #path > 0 then if #status > 0 and #path > 0 then
self.output[utils.path_remove_trailing(utils.path_join({ self.toplevel, path }))] = status self.path_xy[utils.path_remove_trailing(utils.path_join({ self.opts.toplevel, path }))] = status
end end
end end
@ -30,7 +42,7 @@ end
---@param prev_output string ---@param prev_output string
---@param incoming string ---@param incoming string
---@return 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 if incoming and utils.str_find(incoming, "\n") then
local prev = prev_output .. incoming local prev = prev_output .. incoming
local i = 1 local i = 1
@ -45,7 +57,7 @@ function Runner:_handle_incoming_data(prev_output, incoming)
-- skip next line if it is a rename entry -- skip next line if it is a rename entry
skip_next_line = true skip_next_line = true
end end
self:_parse_status_output(status, path) self:parse_status_output(status, path)
end end
i = i + #line i = i + #line
end end
@ -58,35 +70,38 @@ function Runner:_handle_incoming_data(prev_output, incoming)
end end
for line in prev_output:gmatch("[^\n]*\n") do for line in prev_output:gmatch("[^\n]*\n") do
self:_parse_status_output(line) self:parse_status_output(line)
end end
return "" return ""
end end
---@private
---@param stdout_handle uv.uv_pipe_t ---@param stdout_handle uv.uv_pipe_t
---@param stderr_handle uv.uv_pipe_t ---@param stderr_handle uv.uv_pipe_t
---@return table ---@return uv.spawn.options
function Runner:_getopts(stdout_handle, stderr_handle) function GitRunner:get_spawn_options(stdout_handle, stderr_handle)
local untracked = self.list_untracked and "-u" or nil local untracked = self.opts.list_untracked and "-u" or nil
local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no" local ignored = (self.opts.list_untracked and self.opts.list_ignored) and "--ignored=matching" or "--ignored=no"
return { return {
args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path }, args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.opts.path },
cwd = self.toplevel, cwd = self.opts.toplevel,
stdio = { nil, stdout_handle, stderr_handle }, stdio = { nil, stdout_handle, stderr_handle },
} }
end end
---@private
---@param output string ---@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 if log.enabled("git") and output and type(output) == "string" then
log.raw("git", "%s", output) log.raw("git", "%s", output)
log.line("git", "done") log.line("git", "done")
end end
end end
---@private
---@param callback function|nil ---@param callback function|nil
function Runner:_run_git_job(callback) function GitRunner:run_git_job(callback)
local handle, pid local handle, pid
local stdout = vim.loop.new_pipe(false) local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false) local stderr = vim.loop.new_pipe(false)
@ -123,20 +138,20 @@ function Runner:_run_git_job(callback)
end end
end end
local opts = self:_getopts(stdout, stderr) local spawn_options = self:get_spawn_options(stdout, stderr)
log.line("git", "running job with timeout %dms", self.timeout) log.line("git", "running job with timeout %dms", self.opts.timeout)
log.line("git", "git %s", table.concat(utils.array_remove_nils(opts.args), " ")) log.line("git", "git %s", table.concat(utils.array_remove_nils(spawn_options.args), " "))
handle, pid = vim.loop.spawn( handle, pid = vim.loop.spawn(
"git", "git",
opts, spawn_options,
vim.schedule_wrap(function(rc) vim.schedule_wrap(function(rc)
on_finish(rc) on_finish(rc)
end) end)
) )
timer:start( timer:start(
self.timeout, self.opts.timeout,
0, 0,
vim.schedule_wrap(function() vim.schedule_wrap(function()
on_finish(-1) on_finish(-1)
@ -151,19 +166,20 @@ function Runner:_run_git_job(callback)
if data then if data then
data = data:gsub("%z", "\n") data = data:gsub("%z", "\n")
end end
self:_log_raw_output(data) self:log_raw_output(data)
output_leftover = self:_handle_incoming_data(output_leftover, data) output_leftover = self:handle_incoming_data(output_leftover, data)
end end
local function manage_stderr(_, data) local function manage_stderr(_, data)
self:_log_raw_output(data) self:log_raw_output(data)
end end
vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout)) vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout))
vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr)) vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr))
end end
function Runner:_wait() ---@private
function GitRunner:wait()
local function is_done() local function is_done()
return self.rc ~= nil return self.rc ~= nil
end end
@ -172,64 +188,69 @@ function Runner:_wait()
end end
end end
---@param opts table ---@private
function Runner:_finalise(opts) function GitRunner:finalise()
if self.rc == -1 then 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 timeouts = timeouts + 1
if timeouts == MAX_TIMEOUTS then 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() require("nvim-tree.git").disable_git_integration()
end end
elseif self.rc ~= 0 then 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 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
end end
--- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms ---Return nil when callback present
---@param opts table ---@private
---@param callback function|nil executed passing return when complete ---@return GitPathXY?
---@return table|nil status by absolute path, nil if callback present function GitRunner:execute()
function Runner.run(opts, callback) local async = self.opts.callback ~= nil
local self = setmetatable({ local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.opts.toplevel, self.opts.path)
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 if async and self.opts.callback then
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.toplevel, opts.path)
if async and callback then
-- async, always call back -- async, always call back
self:_run_git_job(function() self:run_git_job(function()
log.profile_end(profile) log.profile_end(profile)
self:_finalise(opts) self:finalise()
callback(self.output) self.opts.callback(self.path_xy)
end) end)
else else
-- sync, maybe call back -- sync, maybe call back
self:_run_git_job() self:run_git_job()
self:_wait() self:wait()
log.profile_end(profile) log.profile_end(profile)
self:_finalise(opts) self:finalise()
if callback then if self.opts.callback then
callback(self.output) self.opts.callback(self.path_xy)
else else
return self.output return self.path_xy
end end
end end
end end
return Runner ---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?
function GitRunner:run(opts)
---@type GitRunner
local runner = {
opts = opts,
path_xy = {},
}
runner = GitRunner:new(runner)
return runner:execute()
end
return GitRunner

View File

@ -58,10 +58,11 @@ function M.get_toplevel(cwd)
return toplevel, git_dir return toplevel, git_dir
end end
---@type table<string, boolean>
local untracked = {} local untracked = {}
---@param cwd string ---@param cwd string
---@return string|nil ---@return boolean
function M.should_show_untracked(cwd) function M.should_show_untracked(cwd)
if untracked[cwd] ~= nil then if untracked[cwd] ~= nil then
return untracked[cwd] return untracked[cwd]
@ -81,8 +82,8 @@ function M.should_show_untracked(cwd)
return untracked[cwd] return untracked[cwd]
end end
---@param t table|nil ---@param t table<string|integer, boolean>?
---@param k string ---@param k string|integer
---@return table ---@return table
local function nil_insert(t, k) local function nil_insert(t, k)
t = t or {} t = t or {}
@ -90,31 +91,33 @@ local function nil_insert(t, k)
return t return t
end end
---@param status table ---@param project_files GitProjectFiles
---@param cwd string|nil ---@param cwd string|nil
---@return table ---@return GitProjectDirs
function M.file_status_to_dir_status(status, cwd) function M.project_files_to_project_dirs(project_files, cwd)
local direct = {} ---@type GitProjectDirs
for p, s in pairs(status) do local project_dirs = {}
project_dirs.direct = {}
for p, s in pairs(project_files) do
if s ~= "!!" then if s ~= "!!" then
local modified = vim.fn.fnamemodify(p, ":h") 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
end end
local indirect = {} project_dirs.indirect = {}
for dirname, statuses in pairs(direct) do for dirname, statuses in pairs(project_dirs.direct) do
for s, _ in pairs(statuses) do for s, _ in pairs(statuses) do
local modified = dirname local modified = dirname
while modified ~= cwd and modified ~= "/" do while modified ~= cwd and modified ~= "/" do
modified = vim.fn.fnamemodify(modified, ":h") 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 end
end end
local r = { indirect = indirect, direct = direct } for _, d in pairs(project_dirs) do
for _, d in pairs(r) do
for dirname, statuses in pairs(d) do for dirname, statuses in pairs(d) do
local new_statuses = {} local new_statuses = {}
for s, _ in pairs(statuses) do for s, _ in pairs(statuses) do
@ -123,7 +126,60 @@ function M.file_status_to_dir_status(status, cwd)
d[dirname] = new_statuses d[dirname] = new_statuses
end end
end end
return r
return project_dirs
end
---Git file status for an absolute path
---@param parent_ignored boolean
---@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, project, path, path_fallback)
---@type GitNodeStatus
local ns
if parent_ignored then
ns = {
file = "!!"
}
elseif project and project.files then
ns = {
file = project.files[path] or project.files[path_fallback]
}
else
ns = {}
end
return ns
end
---Git file and directory status for an absolute path
---@param parent_ignored boolean
---@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, project, path, path_fallback)
---@type GitNodeStatus?
local ns
if parent_ignored then
ns = {
file = "!!"
}
elseif project then
ns = {
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
return ns
end end
function M.setup(opts) function M.setup(opts)

View File

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

View File

@ -200,7 +200,8 @@ function Marks:navigate(up)
Iterator.builder(self.explorer.nodes) Iterator.builder(self.explorer.nodes)
:recursor(function(n) :recursor(function(n)
return n.open and n.nodes local dir = n:as(DirectoryNode)
return dir and dir.open and dir.nodes
end) end)
:applier(function(n) :applier(function(n)
if n.absolute_path == node.absolute_path then if n.absolute_path == node.absolute_path then
@ -263,7 +264,7 @@ function Marks:navigate_select()
return return
end end
local node = self.marks[choice] 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) open_file.fn("edit", node.absolute_path)
elseif node then elseif node then
utils.focus_file(node.absolute_path) utils.focus_file(node.absolute_path)

View File

@ -1,10 +1,11 @@
local git = require("nvim-tree.git") local git_utils = require("nvim-tree.git.utils")
local utils = require("nvim-tree.utils")
local DirectoryNode = require("nvim-tree.node.directory") local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DirectoryLinkNode: DirectoryNode ---@class (exact) DirectoryLinkNode: DirectoryNode
---@field link_to string absolute path ---@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() local DirectoryLinkNode = DirectoryNode:new()
---Static factory method ---Static factory method
@ -20,7 +21,7 @@ function DirectoryLinkNode:create(explorer, parent, absolute_path, link_to, name
-- create DirectoryNode with the target path for the watcher -- create DirectoryNode with the target path for the watcher
local o = DirectoryNode:create(explorer, parent, link_to, name, fs_stat) 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 -- reset absolute path to the link itself
o.absolute_path = absolute_path o.absolute_path = absolute_path
@ -36,11 +37,44 @@ function DirectoryLinkNode:destroy()
DirectoryNode.destroy(self) DirectoryNode.destroy(self)
end 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 parent_ignored boolean
-----@param status table|nil ---@param project GitProject?
function DirectoryLinkNode:update_git_status(parent_ignored, status) function DirectoryLinkNode:update_git_status(parent_ignored, project)
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, project, self.link_to, self.absolute_path)
end
---@return HighlightedString name
function DirectoryLinkNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.folder then
return self:highlighted_icon_empty()
end
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
---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, self.explorer.opts.renderer.icons.symlink_arrow, link_to)
name.hl = { "NvimTreeSymlinkFolderName" }
end
return name
end end
---Create a sanitized partial copy of a node, populating children recursively. ---Create a sanitized partial copy of a node, populating children recursively.

View File

@ -1,6 +1,6 @@
local git = require("nvim-tree.git") local git_utils = require("nvim-tree.git.utils")
local watch = require("nvim-tree.explorer.watch") local icons = require("nvim-tree.renderer.components.devicons")
local notify = require("nvim-tree.notify")
local Node = require("nvim-tree.node") local Node = require("nvim-tree.node")
---@class (exact) DirectoryNode: Node ---@class (exact) DirectoryNode: Node
@ -8,8 +8,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 group_next DirectoryNode? -- If node is grouped, this points to the next child dir/link node
---@field nodes Node[] ---@field nodes Node[]
---@field open boolean ---@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 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() local DirectoryNode = Node:new()
---Static factory method ---Static factory method
@ -32,11 +32,11 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat)
fs_stat = fs_stat, fs_stat = fs_stat,
git_status = nil, git_status = nil,
hidden = false, hidden = false,
is_dot = false,
name = name, name = name,
parent = parent, parent = parent,
watcher = nil, watcher = nil,
diag_status = nil, diag_status = nil,
is_dot = false,
has_children = has_children, has_children = has_children,
group_next = nil, group_next = nil,
@ -44,9 +44,9 @@ function DirectoryNode:create(explorer, parent, absolute_path, name, fs_stat)
open = false, open = false,
hidden_stats = nil, hidden_stats = nil,
} }
o = self:new(o) --[[@as DirectoryNode]] o = self:new(o)
o.watcher = watch.create_watcher(o) o.watcher = require("nvim-tree.explorer.watch").create_watcher(o)
return o return o
end end
@ -66,41 +66,41 @@ function DirectoryNode:destroy()
Node.destroy(self) Node.destroy(self)
end end
---Update the GitStatus of the directory ---Update the git_status of the directory
---@param parent_ignored boolean ---@param parent_ignored boolean
---@param status table|nil ---@param project GitProject?
function DirectoryNode:update_git_status(parent_ignored, status) function DirectoryNode:update_git_status(parent_ignored, project)
self.git_status = git.git_status_dir(parent_ignored, status, self.absolute_path, nil) self.git_status = git_utils.git_status_dir(parent_ignored, project, self.absolute_path, nil)
end end
---@return GitStatus|nil ---@return GitXY[]?
function DirectoryNode:get_git_status() function DirectoryNode:get_git_xy()
if not self.git_status or not self.explorer.opts.git.show_on_dirs then if not self.git_status or not self.explorer.opts.git.show_on_dirs then
return nil return nil
end end
local status = {} local xys = {}
if not self:last_group_node().open or self.explorer.opts.git.show_on_open_dirs then 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 -- dir is closed or we should show on open_dirs
if self.git_status.file ~= nil then if self.git_status.file ~= nil then
table.insert(status, self.git_status.file) table.insert(xys, self.git_status.file)
end end
if self.git_status.dir ~= nil then if self.git_status.dir ~= nil then
if self.git_status.dir.direct ~= nil then if self.git_status.dir.direct ~= nil then
for _, s in pairs(self.git_status.dir.direct) do for _, s in pairs(self.git_status.dir.direct) do
table.insert(status, s) table.insert(xys, s)
end end
end end
if self.git_status.dir.indirect ~= nil then if self.git_status.dir.indirect ~= nil then
for _, s in pairs(self.git_status.dir.indirect) do for _, s in pairs(self.git_status.dir.indirect) do
table.insert(status, s) table.insert(xys, s)
end end
end end
end end
else else
-- dir is open and we shouldn't show on open_dirs -- dir is open and we shouldn't show on open_dirs
if self.git_status.file ~= nil then if self.git_status.file ~= nil then
table.insert(status, self.git_status.file) table.insert(xys, self.git_status.file)
end end
if self.git_status.dir ~= nil and self.git_status.dir.direct ~= nil then if self.git_status.dir ~= nil and self.git_status.dir.direct ~= nil then
local deleted = { local deleted = {
@ -111,34 +111,18 @@ function DirectoryNode:get_git_status()
} }
for _, s in pairs(self.git_status.dir.direct) do for _, s in pairs(self.git_status.dir.direct) do
if deleted[s] then if deleted[s] then
table.insert(status, s) table.insert(xys, s)
end end
end end
end end
end end
if #status == 0 then if #xys == 0 then
return nil return nil
else else
return status return xys
end end
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. -- If node is grouped, return the last node in the group. Otherwise, return the given node.
---@return DirectoryNode ---@return DirectoryNode
function DirectoryNode:last_group_node() function DirectoryNode:last_group_node()
@ -191,7 +175,7 @@ function DirectoryNode:ungroup_empty_folders()
end end
end end
---@param toggle_group boolean ---@param toggle_group boolean?
function DirectoryNode:expand_or_collapse(toggle_group) function DirectoryNode:expand_or_collapse(toggle_group)
toggle_group = toggle_group or false toggle_group = toggle_group or false
if self.has_children then if self.has_children then
@ -224,6 +208,84 @@ function DirectoryNode:expand_or_collapse(toggle_group)
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
---@return HighlightedString icon
function DirectoryNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.folder then
return self:highlighted_icon_empty()
end
local str, hl
-- devicon if enabled and available
if self.explorer.opts.renderer.icons.web_devicons.folder.enable then
str, hl = icons.get_icon(self.name)
if not self.explorer.opts.renderer.icons.web_devicons.folder.color then
hl = nil
end
end
-- default icon from opts
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
-- default hl
if not hl then
if self.open then
hl = "NvimTreeOpenedFolderIcon"
else
hl = "NvimTreeClosedFolderIcon"
end
end
return { str = str, hl = { hl } }
end
---@return HighlightedString icon
function DirectoryNode:highlighted_name()
local str, hl
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
str = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
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
hl = "NvimTreeSpecialFolderName"
elseif self.open then
hl = "NvimTreeOpenedFolderName"
elseif #self.nodes == 0 and not self.has_children then
hl = "NvimTreeEmptyFolderName"
end
return { str = str, hl = { hl } }
end
---Create a sanitized partial copy of a node, populating children recursively. ---Create a sanitized partial copy of a node, populating children recursively.
---@return DirectoryNode cloned ---@return DirectoryNode cloned
function DirectoryNode:clone() function DirectoryNode:clone()

View File

@ -1,10 +1,11 @@
local git = require("nvim-tree.git") local git_utils = require("nvim-tree.git.utils")
local utils = require("nvim-tree.utils")
local FileNode = require("nvim-tree.node.file") local FileNode = require("nvim-tree.node.file")
---@class (exact) FileLinkNode: FileNode ---@class (exact) FileLinkNode: FileNode
---@field link_to string absolute path ---@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() local FileLinkNode = FileNode:new()
---Static factory method ---Static factory method
@ -19,7 +20,7 @@ local FileLinkNode = FileNode:new()
function FileLinkNode:create(explorer, parent, absolute_path, link_to, name, fs_stat, fs_stat_target) 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) 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.type = "link"
o.link_to = link_to o.link_to = link_to
@ -32,11 +33,37 @@ function FileLinkNode:destroy()
FileNode.destroy(self) FileNode.destroy(self)
end 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 parent_ignored boolean
-----@param status table|nil ---@param project GitProject?
function FileLinkNode:update_git_status(parent_ignored, status) function FileLinkNode:update_git_status(parent_ignored, project)
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, project, self.link_to, self.absolute_path)
end
---@return HighlightedString icon
function FileLinkNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.file then
return self:highlighted_icon_empty()
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
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, self.explorer.opts.renderer.icons.symlink_arrow, link_to)
end
return { str = str, hl = { "NvimTreeSymlink" } }
end end
---Create a sanitized partial copy of a node ---Create a sanitized partial copy of a node

View File

@ -1,8 +1,18 @@
local git = require("nvim-tree.git") local git_utils = require("nvim-tree.git.utils")
local icons = require("nvim-tree.renderer.components.devicons")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local Node = require("nvim-tree.node") 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 ---@class (exact) FileNode: Node
---@field extension string ---@field extension string
local FileNode = Node:new() local FileNode = Node:new()
@ -24,14 +34,14 @@ function FileNode:create(explorer, parent, absolute_path, name, fs_stat)
fs_stat = fs_stat, fs_stat = fs_stat,
git_status = nil, git_status = nil,
hidden = false, hidden = false,
is_dot = false,
name = name, name = name,
parent = parent, parent = parent,
diag_status = nil, diag_status = nil,
is_dot = false,
extension = string.match(name, ".?[^.]+%.(.*)") or "", extension = string.match(name, ".?[^.]+%.(.*)") or "",
} }
o = self:new(o) --[[@as FileNode]] o = self:new(o)
return o return o
end end
@ -42,13 +52,13 @@ end
---Update the GitStatus of the file ---Update the GitStatus of the file
---@param parent_ignored boolean ---@param parent_ignored boolean
---@param status table|nil ---@param project GitProject?
function FileNode:update_git_status(parent_ignored, status) function FileNode:update_git_status(parent_ignored, project)
self.git_status = git.git_status_file(parent_ignored, status, self.absolute_path, nil) self.git_status = git_utils.git_status_file(parent_ignored, project, self.absolute_path, nil)
end end
---@return GitStatus|nil ---@return GitXY[]?
function FileNode:get_git_status() function FileNode:get_git_xy()
if not self.git_status then if not self.git_status then
return nil return nil
end end
@ -56,6 +66,49 @@ function FileNode:get_git_status()
return self.git_status.file and { self.git_status.file } return self.git_status.file and { self.git_status.file }
end end
---@return HighlightedString icon
function FileNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.file then
return self:highlighted_icon_empty()
end
local str, hl
-- 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, nil, { default = true })
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 = str, hl = { hl } }
end
---@return HighlightedString name
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
hl = "NvimTreeSpecialFile"
elseif self.executable then
hl = "NvimTreeExecFile"
elseif PICTURE_MAP[self.extension] then
hl = "NvimTreeImageFile"
end
return { str = self.name, hl = { hl } }
end
---Create a sanitized partial copy of a node ---Create a sanitized partial copy of a node
---@return FileNode cloned ---@return FileNode cloned
function FileNode:clone() function FileNode:clone()

View File

@ -1,5 +1,3 @@
local git = require("nvim-tree.git")
local Class = require("nvim-tree.class") local Class = require("nvim-tree.class")
---Abstract Node class. ---Abstract Node class.
@ -10,41 +8,28 @@ local Class = require("nvim-tree.class")
---@field absolute_path string ---@field absolute_path string
---@field executable boolean ---@field executable boolean
---@field fs_stat uv.fs_stat.result? ---@field fs_stat uv.fs_stat.result?
---@field git_status GitStatus? ---@field git_status GitNodeStatus?
---@field hidden boolean ---@field hidden boolean
---@field name string ---@field name string
---@field parent DirectoryNode? ---@field parent DirectoryNode?
---@field diag_status DiagStatus? ---@field diag_status DiagStatus?
---@field is_dot boolean cached is_dotfile ---@field private is_dot boolean cached is_dotfile
local Node = Class:new() local Node = Class:new()
function Node:destroy() function Node:destroy()
end end
--luacheck: push ignore 212 ---Update the git_status of the node
---Update the GitStatus of the node ---Abstract
---@param parent_ignored boolean ---@param parent_ignored boolean
---@param status table? ---@param project GitProject?
function Node:update_git_status(parent_ignored, status) ---@diagnostic disable-line: unused-local function Node:update_git_status(parent_ignored, project)
---TODO find a way to declare abstract methods self:nop(parent_ignored, project)
end end
--luacheck: pop ---Short-format statuses
---@return GitXY[]?
---@return GitStatus? function Node:get_git_xy()
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 end
---@return boolean ---@return boolean
@ -66,38 +51,6 @@ function Node:is_dotfile()
return false return false
end 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 ---Get the highest parent of grouped nodes, nil when not grouped
---@return DirectoryNode? ---@return DirectoryNode?
function Node:get_parent_of_group() function Node:get_parent_of_group()
@ -115,6 +68,34 @@ function Node:get_parent_of_group()
end end
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
---Highlighted name for the node
---Empty for base Node
---@return HighlightedString icon
function Node:highlighted_name()
return self:highlighted_name_empty()
end
---Create a sanitized partial copy of a node, populating children recursively. ---Create a sanitized partial copy of a node, populating children recursively.
---@return Node cloned ---@return Node cloned
function Node:clone() function Node:clone()
@ -130,10 +111,10 @@ function Node:clone()
fs_stat = self.fs_stat, fs_stat = self.fs_stat,
git_status = self.git_status, git_status = self.git_status,
hidden = self.hidden, hidden = self.hidden,
is_dot = self.is_dot,
name = self.name, name = self.name,
parent = nil, parent = nil,
diag_status = nil, diag_status = nil,
is_dot = self.is_dot,
} }
return clone return clone

View File

@ -12,7 +12,7 @@ local RootNode = DirectoryNode:new()
function RootNode:create(explorer, absolute_path, name, fs_stat) function RootNode:create(explorer, absolute_path, name, fs_stat)
local o = DirectoryNode:create(explorer, nil, 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 return o
end end

View File

@ -2,9 +2,7 @@ local notify = require("nvim-tree.notify")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory") local DirectoryNode = require("nvim-tree.node.directory")
local FileLinkNode = require("nvim-tree.node.file-link")
local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks") local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks")
local DecoratorCopied = require("nvim-tree.renderer.decorator.copied") 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 DecoratorOpened = require("nvim-tree.renderer.decorator.opened")
local pad = require("nvim-tree.renderer.components.padding") 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 ---@class (exact) HighlightedString
---@field str string ---@field str string
@ -45,6 +33,7 @@ local PICTURE_MAP = {
---@field extmarks table[] extra marks for right icon placement ---@field extmarks table[] extra marks for right icon placement
---@field virtual_lines table[] virtual lines for hidden count display ---@field virtual_lines table[] virtual lines for hidden count display
---@field private explorer Explorer ---@field private explorer Explorer
---@field private opts table
---@field private index number ---@field private index number
---@field private depth number ---@field private depth number
---@field private combined_groups table<string, boolean> combined group names ---@field private combined_groups table<string, boolean> combined group names
@ -99,27 +88,6 @@ function Builder:insert_highlight(groups, start, end_)
table.insert(self.hl_args, { groups, self.index, start, end_ or -1 }) table.insert(self.hl_args, { groups, self.index, start, end_ or -1 })
end 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 ---@private
---@param highlighted_strings HighlightedString[] ---@param highlighted_strings HighlightedString[]
---@return string ---@return string
@ -140,82 +108,6 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
return string return string
end 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
if self.opts.renderer.icons.show.file then
return { str = icon, hl = { "NvimTreeSymlinkIcon" } }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } }
else
return { str = "", hl = {} }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } }
end
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 ---@private
---@param indent_markers HighlightedString[] ---@param indent_markers HighlightedString[]
---@param arrows HighlightedString[]|nil ---@param arrows HighlightedString[]|nil
@ -360,14 +252,7 @@ function Builder:build_line(node, idx, num_children)
local arrows = pad.get_arrows(node) local arrows = pad.get_arrows(node)
-- main components -- main components
local icon, name local icon, name = node:highlighted_icon(), node:highlighted_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
-- highighting -- highighting
local icon_hl_group, name_hl_group = self:add_highlights(node) local icon_hl_group, name_hl_group = self:add_highlights(node)
@ -379,11 +264,12 @@ function Builder:build_line(node, idx, num_children)
self.index = self.index + 1 self.index = self.index + 1
if node:is(DirectoryNode) then local dir = node:as(DirectoryNode)
node = node:last_group_node() if dir then
if node.open then dir = dir:last_group_node()
if dir.open then
self.depth = self.depth + 1 self.depth = self.depth + 1
self:build_lines(node) self:build_lines(dir)
self.depth = self.depth - 1 self.depth = self.depth - 1
end end
end end

View File

@ -0,0 +1,35 @@
---@alias devicons_get_icon fun(name: string, ext: string?, opts: table?): string?, string?
---@alias devicons_setup fun(opts: table?)
---@class (strict) DevIcons?
---@field setup devicons_setup
---@field get_icon devicons_get_icon
local devicons
local M = {}
---Wrapper around nvim-web-devicons, nils if devicons not available
---@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
---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 ok, di = pcall(require, "nvim-web-devicons")
if ok then
devicons = di --[[@as DevIcons]]
-- does nothing if already called i.e. doesn't clobber previous user setup
devicons.setup()
end
end
end
return M

View File

@ -1,6 +1,8 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local diagnostics = require("nvim-tree.diagnostics") local diagnostics = require("nvim-tree.diagnostics")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
-- highlight strings for the icons -- highlight strings for the icons
HS_ICON = {}, HS_ICON = {},
@ -24,7 +26,7 @@ function M.get_highlight(node)
local group local group
local diag_status = diagnostics.get_diag_status(node) 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] group = M.HS_FOLDER[diag_status and diag_status.value]
else else
group = M.HS_FILE[diag_status and diag_status.value] group = M.HS_FILE[diag_status and diag_status.value]

View File

@ -1,129 +0,0 @@
local M = { i = {} }
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? name
local function empty()
return "", nil
end
---@param node Node
---@param has_children boolean
---@return string icon
---@return string? name
local function get_folder_icon_default(node, has_children)
local is_symlink = node.link_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
if has_children then
n = M.config.glyphs.folder.open
else
n = M.config.glyphs.folder.empty_open
end
else
if has_children then
n = M.config.glyphs.folder.default
else
n = M.config.glyphs.folder.empty
end
end
return n, nil
end
---@param node Node
---@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)
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? name
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 fname string
---@param extension 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)
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
-- 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, "%.(.*)"))
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
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
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
end
return M

View File

@ -2,13 +2,13 @@ local M = {}
M.diagnostics = require("nvim-tree.renderer.components.diagnostics") M.diagnostics = require("nvim-tree.renderer.components.diagnostics")
M.full_name = require("nvim-tree.renderer.components.full-name") 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") M.padding = require("nvim-tree.renderer.components.padding")
function M.setup(opts) function M.setup(opts)
M.diagnostics.setup(opts) M.diagnostics.setup(opts)
M.full_name.setup(opts) M.full_name.setup(opts)
M.icons.setup(opts) M.devicons.setup(opts)
M.padding.setup(opts) M.padding.setup(opts)
end end

View File

@ -1,3 +1,5 @@
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
local function check_siblings_for_folder(node, with_arrows) local function check_siblings_for_folder(node, with_arrows)
@ -62,7 +64,7 @@ end
---@param node Node ---@param node Node
---@param markers table ---@param markers table
---@param early_stop integer? ---@param early_stop integer?
---@return HighlightedString[] ---@return HighlightedString
function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop) function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop)
local str = "" local str = ""
@ -90,8 +92,9 @@ function M.get_arrows(node)
local str local str
local hl = "NvimTreeFolderArrowClosed" local hl = "NvimTreeFolderArrowClosed"
if node.nodes then local dir = node:as(DirectoryNode)
if node.open then if dir then
if dir.open then
str = M.config.icons.glyphs.folder["arrow_open"] .. " " str = M.config.icons.glyphs.folder["arrow_open"] .. " "
hl = "NvimTreeFolderArrowOpen" hl = "NvimTreeFolderArrowOpen"
else else

View File

@ -19,7 +19,7 @@ function DecoratorBookmarks:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_bookmarks] or HL_POSITION.none, 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, 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 if opts.renderer.icons.show.bookmarks then
o.icon = { o.icon = {

View File

@ -19,7 +19,7 @@ function DecoratorCopied:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none, hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT.none, icon_placement = ICON_PLACEMENT.none,
} }
o = self:new(o) --[[@as DecoratorCopied]] o = self:new(o)
return o return o
end end

View File

@ -18,7 +18,7 @@ function DecoratorCut:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none, hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT.none, icon_placement = ICON_PLACEMENT.none,
} }
o = self:new(o) --[[@as DecoratorCut]] o = self:new(o)
return o return o
end end

View File

@ -4,6 +4,7 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
local Decorator = require("nvim-tree.renderer.decorator") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
-- highlight groups by severity -- highlight groups by severity
local HG_ICON = { local HG_ICON = {
@ -48,7 +49,7 @@ function DecoratorDiagnostics:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_diagnostics] or HL_POSITION.none, 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, 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 if not o.enabled then
return o return o
@ -98,7 +99,7 @@ function DecoratorDiagnostics:calculate_highlight(node)
end end
local group local group
if node.nodes then if node:is(DirectoryNode) then
group = HG_FOLDER[diag_value] group = HG_FOLDER[diag_value]
else else
group = HG_FILE[diag_value] group = HG_FILE[diag_value]

View File

@ -4,15 +4,22 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
local Decorator = require("nvim-tree.renderer.decorator") 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 ---@field ord number decreasing priority
---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked"
---@alias GitIconsByStatus table<GitStatusStrings, GitHighlightedString> human status
---@alias GitIconsByXY table<GitXY, GitHighlightedString[]> porcelain status
---@alias GitGlyphsByStatus table<GitStatusStrings, string> from opts
---@class (exact) DecoratorGit: Decorator ---@class (exact) DecoratorGit: Decorator
---@field file_hl table<string, string>? by porcelain status e.g. "AM" ---@field file_hl_by_xy table<GitXY, string>?
---@field folder_hl table<string, string>? by porcelain status ---@field folder_hl_by_xy table<GitXY, string>?
---@field icons_by_status HighlightedStringGit[]? by human status ---@field icons_by_status GitIconsByStatus?
---@field icons_by_xy table<string, HighlightedStringGit[]>? by porcelain status ---@field icons_by_xy GitIconsByXY?
local DecoratorGit = Decorator:new() local DecoratorGit = Decorator:new()
---Static factory method ---Static factory method
@ -27,14 +34,14 @@ function DecoratorGit:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_git] or HL_POSITION.none, 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, 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 if not o.enabled then
return o return o
end end
if o.hl_pos ~= HL_POSITION.none then if o.hl_pos ~= HL_POSITION.none then
o:build_hl_table() o:build_file_folder_hl_by_xy()
end end
if opts.renderer.icons.show.git then if opts.renderer.icons.show.git then
@ -49,20 +56,19 @@ function DecoratorGit:create(opts, explorer)
return o return o
end end
---@param glyphs table<string, string> user glyps ---@param glyphs GitGlyphsByStatus
function DecoratorGit:build_icons_by_status(glyphs) function DecoratorGit:build_icons_by_status(glyphs)
self.icons_by_status = { self.icons_by_status = {}
staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }, self.icons_by_status.staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }
unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }, self.icons_by_status.unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }
renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 }, self.icons_by_status.renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 }
deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 }, self.icons_by_status.deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 }
unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 }, self.icons_by_status.unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 }
untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 }, self.icons_by_status.untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 }
ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 }, self.icons_by_status.ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 }
}
end end
---@param icons HighlightedStringGit[] ---@param icons GitIconsByXY
function DecoratorGit:build_icons_by_xy(icons) function DecoratorGit:build_icons_by_xy(icons)
self.icons_by_xy = { self.icons_by_xy = {
["M "] = { icons.staged }, ["M "] = { icons.staged },
@ -100,8 +106,8 @@ function DecoratorGit:build_icons_by_xy(icons)
} }
end end
function DecoratorGit:build_hl_table() function DecoratorGit:build_file_folder_hl_by_xy()
self.file_hl = { self.file_hl_by_xy = {
["M "] = "NvimTreeGitFileStagedHL", ["M "] = "NvimTreeGitFileStagedHL",
["C "] = "NvimTreeGitFileStagedHL", ["C "] = "NvimTreeGitFileStagedHL",
["AA"] = "NvimTreeGitFileStagedHL", ["AA"] = "NvimTreeGitFileStagedHL",
@ -134,9 +140,9 @@ function DecoratorGit:build_hl_table()
[" A"] = "none", [" A"] = "none",
} }
self.folder_hl = {} self.folder_hl_by_xy = {}
for k, v in pairs(self.file_hl) do for k, v in pairs(self.file_hl_by_xy) do
self.folder_hl[k] = v:gsub("File", "Folder") self.folder_hl_by_xy[k] = v:gsub("File", "Folder")
end end
end end
@ -148,19 +154,19 @@ function DecoratorGit:calculate_icons(node)
return nil return nil
end end
local git_status = node:get_git_status() local git_xy = node:get_git_xy()
if git_status == nil then if git_xy == nil then
return nil return nil
end end
local inserted = {} local inserted = {}
local iconss = {} local iconss = {}
for _, s in pairs(git_status) do for _, s in pairs(git_xy) do
local icons = self.icons_by_xy[s] local icons = self.icons_by_xy[s]
if not icons then if not icons then
if self.hl_pos == HL_POSITION.none 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 end
return nil return nil
end end
@ -209,15 +215,15 @@ function DecoratorGit:calculate_highlight(node)
return nil return nil
end end
local git_status = node:get_git_status() local git_xy = node:get_git_xy()
if not git_status then if not git_xy then
return nil return nil
end end
if node.nodes then if node:is(DirectoryNode) then
return self.folder_hl[git_status[1]] return self.folder_hl_by_xy[git_xy[1]]
else else
return self.file_hl[git_status[1]] return self.file_hl_by_xy[git_xy[1]]
end end
end end

View File

@ -1,6 +1,8 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
local Decorator = require("nvim-tree.renderer.decorator") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DecoratorHidden: Decorator ---@class (exact) DecoratorHidden: Decorator
---@field icon HighlightedString? ---@field icon HighlightedString?
@ -18,7 +20,7 @@ function DecoratorHidden:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_hidden] or HL_POSITION.none, 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, 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 if opts.renderer.icons.show.hidden then
o.icon = { o.icon = {
@ -48,7 +50,7 @@ function DecoratorHidden:calculate_highlight(node)
return nil return nil
end end
if node.nodes then if node:is(DirectoryNode) then
return "NvimTreeHiddenFolderHL" return "NvimTreeHiddenFolderHL"
else else
return "NvimTreeHiddenFileHL" return "NvimTreeHiddenFileHL"

View File

@ -4,6 +4,7 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
local Decorator = require("nvim-tree.renderer.decorator") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DecoratorModified: Decorator ---@class (exact) DecoratorModified: Decorator
---@field icon HighlightedString|nil ---@field icon HighlightedString|nil
@ -21,7 +22,7 @@ function DecoratorModified:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_modified] or HL_POSITION.none, 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, 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 if not o.enabled then
return o return o
@ -55,7 +56,7 @@ function DecoratorModified:calculate_highlight(node)
return nil return nil
end end
if node.nodes then if node:is(DirectoryNode) then
return "NvimTreeModifiedFolderHL" return "NvimTreeModifiedFolderHL"
else else
return "NvimTreeModifiedFileHL" return "NvimTreeModifiedFileHL"

View File

@ -21,7 +21,7 @@ function DecoratorOpened:create(opts, explorer)
hl_pos = HL_POSITION[opts.renderer.highlight_opened_files] or HL_POSITION.none, hl_pos = HL_POSITION[opts.renderer.highlight_opened_files] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT.none, icon_placement = ICON_PLACEMENT.none,
} }
o = self:new(o) --[[@as DecoratorOpened]] o = self:new(o)
return o return o
end end

View File

@ -2,8 +2,6 @@ local log = require("nvim-tree.log")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local icon_component = require("nvim-tree.renderer.components.icons")
local Builder = require("nvim-tree.renderer.builder") local Builder = require("nvim-tree.renderer.builder")
local SIGN_GROUP = "NvimTreeRendererSigns" local SIGN_GROUP = "NvimTreeRendererSigns"
@ -16,7 +14,6 @@ local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtua
---@field private __index? table ---@field private __index? table
---@field private opts table user options ---@field private opts table user options
---@field private explorer Explorer ---@field private explorer Explorer
---@field private builder Builder
local Renderer = {} local Renderer = {}
---@param opts table user options ---@param opts table user options
@ -27,7 +24,6 @@ function Renderer:new(opts, explorer)
local o = { local o = {
opts = opts, opts = opts,
explorer = explorer, explorer = explorer,
builder = Builder:new(opts, explorer),
} }
setmetatable(o, self) setmetatable(o, self)
@ -109,7 +105,6 @@ function Renderer:draw()
local profile = log.profile_start("draw") local profile = log.profile_start("draw")
local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0) 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() local builder = Builder:new(self.opts, self.explorer):build()

View File

@ -150,12 +150,30 @@ end
local function set_window_options_and_buffer() local function set_window_options_and_buffer()
pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr()) pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr())
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore = "all" if vim.fn.has("nvim-0.10") == 1 then
for k, v in pairs(M.View.winopts) do
vim.opt_local[k] = v 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 end
vim.opt.eventignore = eventignore
end end
---@return table ---@return table

View File

@ -2,21 +2,7 @@ local notify = require("nvim-tree.notify")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local M = { local Class = require("nvim-tree.class")
config = {},
}
---@class Event
local Event = {
_events = {},
}
Event.__index = Event
---@class Watcher
local Watcher = {
_watchers = {},
}
Watcher.__index = Watcher
local FS_EVENT_FLAGS = { local FS_EVENT_FLAGS = {
-- inotify or equivalent will be used; fallback to stat has not yet been implemented -- inotify or equivalent will be used; fallback to stat has not yet been implemented
@ -25,20 +11,40 @@ local FS_EVENT_FLAGS = {
recursive = false, 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 ---@param path string
---@return Event|nil ---@return Event|nil
function Event:new(path) function Event:create(path)
log.line("watcher", "Event:new '%s'", path) log.line("watcher", "Event:create '%s'", path)
local e = setmetatable({ ---@type Event
_path = path, local o = {
_fs_event = nil, destroyed = false,
_listeners = {}, path = path,
}, Event) fs_event = nil,
listeners = {},
}
o = self:new(o)
if e:start() then if o:start() then
Event._events[path] = e events[path] = o
return e return o
else else
return nil return nil
end end
@ -46,21 +52,21 @@ end
---@return boolean ---@return boolean
function Event:start() function Event:start()
log.line("watcher", "Event:start '%s'", self._path) log.line("watcher", "Event:start '%s'", self.path)
local rc, _, name local rc, _, name
self._fs_event, _, name = vim.loop.new_fs_event() self.fs_event, _, name = vim.loop.new_fs_event()
if not self._fs_event then if not self.fs_event then
self._fs_event = nil self.fs_event = nil
notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name)) notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self.path, name))
return false return false
end end
local event_cb = vim.schedule_wrap(function(err, filename) local event_cb = vim.schedule_wrap(function(err, filename)
if err then if err then
log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self._path, filename, err) 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) 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 if err == "EPERM" and (utils.is_windows or utils.is_wsl) then
-- on directory removal windows will cascade the filesystem events out of order -- on directory removal windows will cascade the filesystem events out of order
log.line("watcher", message) log.line("watcher", message)
@ -69,19 +75,19 @@ function Event:start()
self:destroy(message) self:destroy(message)
end end
else else
log.line("watcher", "event_cb '%s' '%s'", self._path, filename) log.line("watcher", "event_cb '%s' '%s'", self.path, filename)
for _, listener in ipairs(self._listeners) do for _, listener in ipairs(self.listeners) do
listener(filename) listener(filename)
end end
end 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 rc ~= 0 then
if name == "EMFILE" 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") M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting")
else 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 end
return false return false
end end
@ -91,81 +97,105 @@ end
---@param listener function ---@param listener function
function Event:add(listener) function Event:add(listener)
table.insert(self._listeners, listener) table.insert(self.listeners, listener)
end end
---@param listener function ---@param listener function
function Event:remove(listener) function Event:remove(listener)
utils.array_remove(self._listeners, listener) utils.array_remove(self.listeners, listener)
if #self._listeners == 0 then if #self.listeners == 0 then
self:destroy() self:destroy()
end end
end end
---@param message string|nil ---@param message string|nil
function Event:destroy(message) 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 if message then
notify.warn(message) notify.warn(message)
end end
local rc, _, name = self._fs_event:stop() local rc, _, name = self.fs_event:stop()
if rc ~= 0 then 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 end
self._fs_event = nil self.fs_event = nil
end end
Event._events[self._path] = nil
self.destroyed = true self.destroyed = true
events[self.path] = nil
end 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 path string
---@param files string[]|nil ---@param files string[]|nil
---@param callback function ---@param callback fun(watcher: Watcher)
---@param data table ---@param data table user data
---@return Watcher|nil ---@return Watcher|nil
function Watcher:new(path, files, callback, data) function Watcher:create(path, files, callback, data)
log.line("watcher", "Watcher:new '%s' %s", path, vim.inspect(files)) log.line("watcher", "Watcher:create '%s' %s", path, vim.inspect(files))
local w = setmetatable(data, Watcher) local event = events[path] or Event:create(path)
if not event then
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
return nil return nil
end 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)
table.insert(Watcher._watchers, w) o:start()
return w table.insert(watchers, o)
return o
end end
function Watcher:start() function Watcher:start()
self._listener = function(filename) self.listener = function(filename)
if not self._files or vim.tbl_contains(self._files, filename) then if not self.files or vim.tbl_contains(self.files, filename) then
self._callback(self) self.callback(self)
end end
end end
self._event:add(self._listener) self.event:add(self.listener)
end end
function Watcher:destroy() 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 self.destroyed = true
end end
@ -183,11 +213,11 @@ end
function M.purge_watchers() function M.purge_watchers()
log.line("watcher", "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() w:destroy()
end end
for _, e in pairs(Event._events) do for _, e in pairs(events) do
e:destroy() e:destroy()
end end
end end