diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index b7000a3eb3b..b89e947e4ca 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -423,6 +423,7 @@ Following is the default configuration. See |nvim-tree-opts| for details. root_folder_label = ":~:s?$?/..?", indent_width = 2, special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, + hidden_display = "none", symlink_destination = true, highlight_git = "none", highlight_diagnostics = "none", @@ -878,6 +879,49 @@ Number of spaces for an each tree nesting level. Minimum 1. A list of filenames that gets highlighted with `NvimTreeSpecialFile`. Type: `table`, Default: `{ "Cargo.toml", "Makefile", "README.md", "readme.md", }` +*nvim-tree.renderer.hidden_display* +Show a summary of hidden files below the tree using `NvimTreeHiddenDisplay + Type: `function | string`, Default: `"none"` + + Possible string values are: + - `"none"`: Doesn't inform anything about hidden files. + - `"simple"`: Shows how many hidden files are in a folder. + - `"all"`: Shows how many files are hidden and the number of hidden + files per reason why they're hidden. + + Example `"all"`: + If a folder has 14 hidden items for various reasons, the display might + show: > + (14 total git: 5, dotfile: 9) +< + If a function is provided, it receives a table `hidden_stats` where keys are + reasons and values are the count of hidden files for that reason. + + The `hidden_stats` argument is structured as follows, where <num> is the + number of hidden files related to the field: > + hidden_stats = { + bookmark = <num>, + buf = <num>, + custom = <num>, + dotfile = <num>, + git = <num>, + live_filter = <num>, + } +< + Example of function that can be passed: > + function(hidden_stats) + local total_count = 0 + for reason, count in pairs(hidden_stats) do + total_count = total_count + count + end + + if total_count > 0 then + return "(" .. tostring(total_count) .. " hidden)" + end + return nil + end +< + *nvim-tree.renderer.symlink_destination* Whether to show the destination of the symlink. Type: `boolean`, Default: `true` @@ -2461,6 +2505,9 @@ Hidden: > NvimTreeModifiedFileHL NvimTreeHiddenIcon NvimTreeModifiedFolderHL NvimTreeHiddenFileHL < +Hidden Display: > + NvimTreeHiddenDisplay Conceal +< Opened: > NvimTreeOpenedHL Special < @@ -2872,6 +2919,7 @@ highlight group is not, hard linking as follows: > |nvim-tree.renderer.add_trailing| |nvim-tree.renderer.full_name| |nvim-tree.renderer.group_empty| +|nvim-tree.renderer.hidden_display| |nvim-tree.renderer.highlight_bookmarks| |nvim-tree.renderer.highlight_clipboard| |nvim-tree.renderer.highlight_diagnostics| diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 5eeef047ad3..f543ae8ae1e 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -398,6 +398,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS root_folder_label = ":~:s?$?/..?", indent_width = 2, special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, + hidden_display = "none", symlink_destination = true, highlight_git = "none", highlight_diagnostics = "none", @@ -647,6 +648,7 @@ local ACCEPTED_TYPES = { }, }, renderer = { + hidden_display = { "function", "string" }, group_empty = { "boolean", "function" }, root_folder_label = { "function", "string", "boolean" }, }, @@ -680,6 +682,7 @@ local ACCEPTED_STRINGS = { signcolumn = { "yes", "no", "auto" }, }, renderer = { + hidden_display = { "none", "simple", "all" }, highlight_git = { "none", "icon", "name", "all" }, highlight_opened_files = { "none", "icon", "name", "all" }, highlight_modified = { "none", "icon", "name", "all" }, diff --git a/lua/nvim-tree/appearance/init.lua b/lua/nvim-tree/appearance/init.lua index 4b4f33ff597..9a8c67efe67 100644 --- a/lua/nvim-tree/appearance/init.lua +++ b/lua/nvim-tree/appearance/init.lua @@ -14,6 +14,7 @@ M.HIGHLIGHT_GROUPS = { -- Standard { group = "NvimTreeNormal", link = "Normal" }, { group = "NvimTreeNormalFloat", link = "NormalFloat" }, + { group = "NvimTreeNormalFloatBorder", link = "FloatBorder" }, { group = "NvimTreeNormalNC", link = "NvimTreeNormal" }, { group = "NvimTreeLineNr", link = "LineNr" }, @@ -81,6 +82,9 @@ M.HIGHLIGHT_GROUPS = { { group = "NvimTreeHiddenFileHL", link = "NvimTreeHiddenIcon" }, { group = "NvimTreeHiddenFolderHL", link = "NvimTreeHiddenFileHL" }, + -- Hidden Display + { group = "NvimTreeHiddenDisplay", link = "Conceal" }, + -- Opened { group = "NvimTreeOpenedHL", link = "Special" }, diff --git a/lua/nvim-tree/enum.lua b/lua/nvim-tree/enum.lua index 054c463f46c..9c50bc27638 100644 --- a/lua/nvim-tree/enum.lua +++ b/lua/nvim-tree/enum.lua @@ -19,4 +19,15 @@ M.ICON_PLACEMENT = { right_align = 4, } +---Reason for filter in filter.lua +---@enum FILTER_REASON +M.FILTER_REASON = { + none = 0, -- It's not filtered + git = 1, + buf = 2, + dotfile = 4, + custom = 8, + bookmark = 16, +} + return M diff --git a/lua/nvim-tree/explorer/explore.lua b/lua/nvim-tree/explorer/explore.lua index 3d48f95a583..4de172536e5 100644 --- a/lua/nvim-tree/explorer/explore.lua +++ b/lua/nvim-tree/explorer/explore.lua @@ -5,6 +5,7 @@ local git = require "nvim-tree.git" local live_filter = require "nvim-tree.live-filter" local log = require "nvim-tree.log" +local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON local Watcher = require "nvim-tree.watcher" local M = {} @@ -17,7 +18,17 @@ local M = {} local function populate_children(handle, cwd, node, git_status, parent) local node_ignored = explorer_node.is_git_ignored(node) local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") + local filter_status = parent.filters:prepare(git_status) + + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }) + while true do local name, t = vim.loop.fs_scandir_next(handle) if not name then @@ -29,8 +40,8 @@ local function populate_children(handle, cwd, node, git_status, parent) ---@type uv.fs_stat.result|nil local stat = vim.loop.fs_stat(abs) - - if not parent.filters:should_filter(abs, stat, filter_status) and not nodes_by_path[abs] and Watcher.is_fs_event_capable(abs) then + local filter_reason = parent.filters:should_filter_as_reason(abs, stat, filter_status) + if filter_reason == FILTER_REASON.none and not nodes_by_path[abs] and Watcher.is_fs_event_capable(abs) then local child = nil if t == "directory" and vim.loop.fs_access(abs, "R") then child = builders.folder(node, abs, name, stat) @@ -47,6 +58,12 @@ local function populate_children(handle, cwd, node, git_status, parent) nodes_by_path[child.absolute_path] = true explorer_node.update_git_status(child, node_ignored, git_status) end + else + for reason, value in pairs(FILTER_REASON) do + if filter_reason == value then + node.hidden_stats[reason] = node.hidden_stats[reason] + 1 + end + end end log.profile_end(profile) diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua index a1eb80da784..6f2cd6df0d0 100644 --- a/lua/nvim-tree/explorer/filters.lua +++ b/lua/nvim-tree/explorer/filters.lua @@ -1,4 +1,5 @@ local utils = require "nvim-tree.utils" +local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON ---@class Filters to handle all opts.filters and related API ---@field config table hydrated user opts.filters @@ -223,4 +224,33 @@ function Filters:should_filter(path, fs_stat, status) or bookmark(self, path, fs_stat and fs_stat.type, status.bookmarks) end +--- Check if the given path should be filtered, and provide the reason why it was +---@param path string Absolute path +---@param fs_stat uv.fs_stat.result|nil fs_stat of file +---@param status table from prepare +---@return FILTER_REASON +function Filters:should_filter_as_reason(path, fs_stat, status) + if not self.config.enable then + return FILTER_REASON.none + end + + if is_excluded(self, path) then + return FILTER_REASON.none + end + + if git(self, path, status.git_status) then + return FILTER_REASON.git + elseif buf(self, path, status.bufinfo) then + return FILTER_REASON.buf + elseif dotfile(self, path) then + return FILTER_REASON.dotfile + elseif custom(self, path) then + return FILTER_REASON.custom + elseif bookmark(self, path, fs_stat and fs_stat.type, status.bookmarks) then + return FILTER_REASON.bookmark + else + return FILTER_REASON.none + end +end + return Filters diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua index 101a22cf000..d76c3234abc 100644 --- a/lua/nvim-tree/explorer/reload.lua +++ b/lua/nvim-tree/explorer/reload.lua @@ -5,6 +5,7 @@ local live_filter = require "nvim-tree.live-filter" local git = require "nvim-tree.git" local log = require "nvim-tree.log" +local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON local NodeIterator = require "nvim-tree.iterators.node-iterator" local Watcher = require "nvim-tree.watcher" @@ -92,6 +93,16 @@ function M.reload(node, git_status) local node_ignored = explorer_node.is_git_ignored(node) ---@type table<string, Node> local nodes_by_path = utils.key_by(node.nodes, "absolute_path") + + -- To reset we must 'zero' everything that we use + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }) + while true do local name, t = vim.loop.fs_scandir_next(handle) if not name then @@ -102,7 +113,8 @@ function M.reload(node, git_status) ---@type uv.fs_stat.result|nil local stat = vim.loop.fs_stat(abs) - if not explorer.filters:should_filter(abs, stat, filter_status) then + local filter_reason = explorer.filters:should_filter_as_reason(abs, stat, filter_status) + if filter_reason == FILTER_REASON.none then remain_childs[abs] = true -- Recreate node if type changes. @@ -139,6 +151,12 @@ function M.reload(node, git_status) n.fs_stat = stat end end + else + for reason, value in pairs(FILTER_REASON) do + if filter_reason == value then + node.hidden_stats[reason] = node.hidden_stats[reason] + 1 + end + end end end diff --git a/lua/nvim-tree/live-filter.lua b/lua/nvim-tree/live-filter.lua index cc305da6af4..5e4fef8ab3c 100644 --- a/lua/nvim-tree/live-filter.lua +++ b/lua/nvim-tree/live-filter.lua @@ -18,10 +18,17 @@ local function reset_filter(node_) return end + node_.hidden_stats = vim.tbl_deep_extend("force", node_.hidden_stats or {}, { + live_filter = 0, + }) + Iterator.builder(node_.nodes) :hidden() :applier(function(node) node.hidden = false + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + live_filter = 0, + }) end) :iterate() end @@ -79,6 +86,10 @@ function M.apply_filter(node_) local filtered_nodes = 0 local nodes = node.group_next and { node.group_next } or node.nodes + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + live_filter = 0, + }) + if nodes then for _, n in pairs(nodes) do iterate(n) @@ -88,6 +99,8 @@ function M.apply_filter(node_) end end + node.hidden_stats.live_filter = filtered_nodes + local has_nodes = nodes and (M.always_show_folders or #nodes > filtered_nodes) local ok, is_match = pcall(matches, node) node.hidden = not (has_nodes or (ok and is_match)) diff --git a/lua/nvim-tree/node.lua b/lua/nvim-tree/node.lua index f1344fe9e33..cf8aa671c39 100644 --- a/lua/nvim-tree/node.lua +++ b/lua/nvim-tree/node.lua @@ -21,6 +21,7 @@ ---@field group_next Node|nil ---@field nodes Node[] ---@field open boolean +---@field hidden_stats table -- Each field of this table is a key for source and value for count ---@class FileNode: BaseNode ---@field extension string diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua index 940d2b7523d..749af930447 100644 --- a/lua/nvim-tree/renderer/builder.lua +++ b/lua/nvim-tree/renderer/builder.lua @@ -43,6 +43,8 @@ local M = { ---@field lines string[] includes icons etc. ---@field hl_args AddHighlightArgs[] line highlights ---@field signs string[] line signs +---@field extmarks table[] extra marks for right icon placement +---@field virtual_lines table[] virtual lines for hidden count display ---@field private root_cwd string absolute path ---@field private index number ---@field private depth number @@ -62,6 +64,7 @@ function Builder:new() markers = {}, signs = {}, extmarks = {}, + virtual_lines = {}, } setmetatable(o, self) self.__index = self @@ -351,7 +354,6 @@ function Builder:build_line(node, idx, num_children) self.index = self.index + 1 node = require("nvim-tree.lib").get_last_group_node(node) - if node.open then self.depth = self.depth + 1 self:build_lines(node) @@ -359,6 +361,32 @@ function Builder:build_line(node, idx, num_children) end end +---Add virtual lines for rendering hidden count information per node +---@private +function Builder:add_hidden_count_string(node, idx, num_children) + if not node.open then + return + end + local hidden_count_string = M.opts.renderer.hidden_display(node.hidden_stats) + if hidden_count_string and hidden_count_string ~= "" then + local indent_markers = pad.get_indent_markers(self.depth, idx or 0, num_children or 0, node, self.markers, 1) + local indent_width = M.opts.renderer.indent_width + + local indent_padding = string.rep(" ", indent_width) + local indent_string = indent_padding .. indent_markers.str + local line_nr = #self.lines - 1 + self.virtual_lines[line_nr] = self.virtual_lines[line_nr] or {} + + -- NOTE: We are inserting in depth order because of current traversal + -- if we change the traversal, we might need to sort by depth before rendering `self.virtual_lines` + -- to maintain proper ordering of parent and child folder hidden count info. + table.insert(self.virtual_lines[line_nr], { + { indent_string, indent_markers.hl }, + { string.rep(indent_padding, (node.parent == nil and 0 or 1)) .. hidden_count_string, "NvimTreeHiddenDisplay" }, + }) + end +end + ---@private function Builder:get_nodes_number(nodes) if not live_filter.filter then @@ -388,6 +416,7 @@ function Builder:build_lines(node) idx = idx + 1 end end + self:add_hidden_count_string(node) end ---@private @@ -442,7 +471,50 @@ function Builder:build() return self end +---@param opts table +local setup_hidden_display_function = function(opts) + local hidden_display = opts.renderer.hidden_display + -- options are already validated, so ´hidden_display´ can ONLY be `string` or `function` if type(hidden_display) == "string" then + if type(hidden_display) == "string" then + if hidden_display == "none" then + opts.renderer.hidden_display = function() + return nil + end + elseif hidden_display == "simple" then + opts.renderer.hidden_display = function(hidden_stats) + return utils.default_format_hidden_count(hidden_stats, true) + end + elseif hidden_display == "all" then + opts.renderer.hidden_display = function(hidden_stats) + return utils.default_format_hidden_count(hidden_stats, false) + end + end + elseif type(hidden_display) == "function" then + local safe_render = function(hidden_stats) + -- In case of missing field such as live_filter we zero it, otherwise keep field as is + hidden_stats = vim.tbl_deep_extend("force", { + live_filter = 0, + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }, hidden_stats or {}) + + local ok, result = pcall(hidden_display, hidden_stats) + if not ok then + notify.warn "Problem occurred in the function ``opts.renderer.hidden_display`` see nvim-tree.renderer.hidden_display on :h nvim-tree" + return nil + end + return result + end + + opts.renderer.hidden_display = safe_render + end +end + function Builder.setup(opts) + setup_hidden_display_function(opts) M.opts = opts -- priority order diff --git a/lua/nvim-tree/renderer/components/padding.lua b/lua/nvim-tree/renderer/components/padding.lua index d61a0b508de..fd51813bc42 100644 --- a/lua/nvim-tree/renderer/components/padding.lua +++ b/lua/nvim-tree/renderer/components/padding.lua @@ -19,7 +19,7 @@ local function check_siblings_for_folder(node, with_arrows) return false end -local function get_padding_indent_markers(depth, idx, nodes_number, markers, with_arrows, inline_arrows, node) +local function get_padding_indent_markers(depth, idx, nodes_number, markers, with_arrows, inline_arrows, node, early_stop) local base_padding = with_arrows and (not node.nodes or depth > 0) and " " or "" local padding = (inline_arrows or depth == 0) and base_padding or "" @@ -27,7 +27,7 @@ local function get_padding_indent_markers(depth, idx, nodes_number, markers, wit local has_folder_sibling = check_siblings_for_folder(node, with_arrows) local indent = string.rep(" ", M.config.indent_width - 1) markers[depth] = idx ~= nodes_number - for i = 1, depth do + for i = 1, depth - early_stop do local glyph if idx == nodes_number and i == depth then local bottom_width = M.config.indent_width - 2 + (with_arrows and not inline_arrows and has_folder_sibling and 2 or 0) @@ -62,7 +62,7 @@ end ---@param node table ---@param markers table ---@return HighlightedString[] -function M.get_indent_markers(depth, idx, nodes_number, node, markers) +function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop) local str = "" local show_arrows = M.config.icons.show.folder_arrow @@ -71,7 +71,7 @@ function M.get_indent_markers(depth, idx, nodes_number, node, markers) local indent_width = M.config.indent_width if show_markers then - str = str .. get_padding_indent_markers(depth, idx, nodes_number, markers, show_arrows, inline_arrows, node) + str = str .. get_padding_indent_markers(depth, idx, nodes_number, markers, show_arrows, inline_arrows, node, early_stop or 0) else str = str .. string.rep(" ", depth * indent_width) end diff --git a/lua/nvim-tree/renderer/init.lua b/lua/nvim-tree/renderer/init.lua index 88ccae6b3f4..729164aea5b 100644 --- a/lua/nvim-tree/renderer/init.lua +++ b/lua/nvim-tree/renderer/init.lua @@ -14,12 +14,13 @@ local SIGN_GROUP = "NvimTreeRendererSigns" local namespace_highlights_id = vim.api.nvim_create_namespace "NvimTreeHighlights" local namespace_extmarks_id = vim.api.nvim_create_namespace "NvimTreeExtmarks" +local namespace_virtual_lines_id = vim.api.nvim_create_namespace "NvimTreeVirtualLines" ---@param bufnr number ---@param lines string[] ---@param hl_args AddHighlightArgs[] ---@param signs string[] -local function _draw(bufnr, lines, hl_args, signs, extmarks) +local function _draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines) if vim.fn.has "nvim-0.10" == 1 then vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr }) else @@ -50,6 +51,15 @@ local function _draw(bufnr, lines, hl_args, signs, extmarks) }) end end + + vim.api.nvim_buf_clear_namespace(bufnr, namespace_virtual_lines_id, 0, -1) + for line_nr, vlines in pairs(virtual_lines) do + vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, { + virt_lines = vlines, + virt_lines_above = false, + virt_lines_leftcol = true, + }) + end end function M.render_hl(bufnr, hl) @@ -79,7 +89,7 @@ function M.draw() local builder = Builder:new():build() - _draw(bufnr, builder.lines, builder.hl_args, builder.signs, builder.extmarks) + _draw(bufnr, builder.lines, builder.hl_args, builder.signs, builder.extmarks, builder.virtual_lines) if cursor and #builder.lines >= cursor[1] then vim.api.nvim_win_set_cursor(view.get_winnr() or 0, cursor) diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index 30f34edc44a..c9711c4bff2 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -181,6 +181,26 @@ function M.get_parent_of_group(node) return node end +M.default_format_hidden_count = function(hidden_count, simple) + local parts = {} + local total_count = 0 + for reason, count in pairs(hidden_count) do + total_count = total_count + count + if count > 0 then + table.insert(parts, reason .. ": " .. tostring(count)) + end + end + + local hidden_count_string = table.concat(parts, ", ") -- if empty then is "" (empty string) + if simple then + hidden_count_string = "" + end + if total_count > 0 then + return "(" .. tostring(total_count) .. (simple and " hidden" or " total ") .. hidden_count_string .. ")" + end + return nil +end + --- Return visible nodes indexed by line ---@param nodes_all Node[] ---@param line_start number diff --git a/lua/nvim-tree/view.lua b/lua/nvim-tree/view.lua index 6cfa1dde3f2..1e9a412a875 100644 --- a/lua/nvim-tree/view.lua +++ b/lua/nvim-tree/view.lua @@ -50,6 +50,7 @@ M.View = { "Normal:NvimTreeNormal", "NormalNC:NvimTreeNormalNC", "NormalFloat:NvimTreeNormalFloat", + "FloatBorder:NvimTreeNormalFloatBorder", }, ","), }, }