diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index f7eb96f6163..9738356c6fc 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -296,7 +296,7 @@ Subsequent calls to setup will replace the previous configuration. debounce_delay = 50, severity = { min = vim.diagnostic.severity.HINT, - max = vim.diagnostic.severity.ERROR + max = vim.diagnostic.severity.ERROR, }, icons = { hint = "", @@ -307,6 +307,8 @@ Subsequent calls to setup will replace the previous configuration. }, filters = { dotfiles = false, + git_clean = false, + no_buffer = false, custom = {}, exclude = {}, }, @@ -903,6 +905,19 @@ Filtering options. Toggle via the `toggle_dotfiles` action, default mapping `H`. Type: `boolean`, Default: `false` + *nvim-tree.filters.git_clean* + Do not show files with no git status. This will show ignored files when + |nvim-tree.git.ignore| is set, as they are effectively dirty. + Toggle via the `toggle_git_clean` action, default mapping `C`. + Type: `boolean`, Default: `false` + + *nvim-tree.filters.no_buffer* + Do not show files that have no listed buffer. + Toggle via the `toggle_no_buffer` action, default mapping `B`. + For performance reasons this may not immediately update on buffer + delete/wipe. A reload or filesystem event will result in an update. + Type: `boolean`, Default: `false` + *nvim-tree.filters.custom* Custom list of vim regex for file/directory names that will not be shown. Backslashes must be escaped e.g. "^\\.git". See |string-match|. @@ -1188,6 +1203,8 @@ exists. - collapse_all `(keep_buffers?: bool)` - expand_all - toggle_gitignore_filter + - toggle_git_clean_filter + - toggle_no_buffer_filter - toggle_custom_filter - toggle_hidden_filter - toggle_help @@ -1302,55 +1319,57 @@ Single right / middle mouse mappings will requre changes to |mousemodel| or |mou DEFAULT MAPPINGS *nvim-tree-default-mappings* -`` edit open a file or folder; root will cd to the above directory +`` edit open a file or folder; root will cd to the above directory `o` `<2-LeftMouse>` -`` edit_in_place edit the file in place, effectively replacing the tree explorer -`O` edit_no_picker same as (edit) with no window picker -`` cd cd in the directory under the cursor +`` edit_in_place edit the file in place, effectively replacing the tree explorer +`O` edit_no_picker same as (edit) with no window picker +`` cd cd in the directory under the cursor `<2-RightMouse>` -`` vsplit open the file in a vertical split -`` split open the file in a horizontal split -`` tabnew open the file in a new tab -`<` prev_sibling navigate to the previous sibling of current file/directory -`>` next_sibling navigate to the next sibling of current file/directory -`P` parent_node move cursor to the parent directory -`` close_node close current opened directory or parent -`` preview open the file as a preview (keeps the cursor in the tree) -`K` first_sibling navigate to the first sibling of current file/directory -`J` last_sibling navigate to the last sibling of current file/directory -`I` toggle_git_ignored toggle visibility of files/folders hidden via |git.ignore| option -`H` toggle_dotfiles toggle visibility of dotfiles via |filters.dotfiles| option -`U` toggle_custom toggle visibility of files/folders hidden via |filters.custom| option -`R` refresh refresh the tree -`a` create add a file; leaving a trailing `/` will add a directory -`d` remove delete a file (will prompt for confirmation) -`D` trash trash a file via |trash| option -`r` rename rename a file -`` full_rename rename a file and omit the filename on input -`x` cut add/remove file/directory to cut clipboard -`c` copy add/remove file/directory to copy clipboard -`p` paste paste from clipboard; cut clipboard has precedence over copy; will prompt for confirmation -`y` copy_name copy name to system clipboard -`Y` copy_path copy relative path to system clipboard -`gy` copy_absolute_path copy absolute path to system clipboard -`[e` prev_diag_item go to next diagnostic item -`[c` prev_git_item go to next git item -`]e` next_diag_item go to prev diagnostic item -`]c` next_git_item go to prev git item -`-` dir_up navigate up to the parent directory of the current file/directory -`s` system_open open a file with default system application or a folder with default file manager, using |system_open| option -`f` live_filter live filter nodes dynamically based on regex matching. -`F` clear_live_filter clear live filter -`q` close close tree window -`W` collapse_all collapse the whole tree -`E` expand_all expand the whole tree, stopping after expanding |actions.expand_all.max_folder_discovery| folders; this might hang neovim for a while if running on a big folder -`S` search_node prompt the user to enter a path and then expands the tree to match the path -`.` run_file_command enter vim command mode with the file the cursor is on -`` toggle_file_info toggle a popup with file infos about the file under the cursor -`g?` toggle_help toggle help -`m` toggle_mark Toggle node in bookmarks -`bmv` bulk_move Move all bookmarked nodes into specified location +`` vsplit open the file in a vertical split +`` split open the file in a horizontal split +`` tabnew open the file in a new tab +`<` prev_sibling navigate to the previous sibling of current file/directory +`>` next_sibling navigate to the next sibling of current file/directory +`P` parent_node move cursor to the parent directory +`` close_node close current opened directory or parent +`` preview open the file as a preview (keeps the cursor in the tree) +`K` first_sibling navigate to the first sibling of current file/directory +`J` last_sibling navigate to the last sibling of current file/directory +`C` toggle_git_clean toggle visibility of git clean via |filters.git_clean| option +`I` toggle_git_ignored toggle visibility of files/folders hidden via |git.ignore| option +`H` toggle_dotfiles toggle visibility of dotfiles via |filters.dotfiles| option +`B` toggle_no_buffer toggle visibility of files/folders hidden via |filters.no_buffer| option +`U` toggle_custom toggle visibility of files/folders hidden via |filters.custom| option +`R` refresh refresh the tree +`a` create add a file; leaving a trailing `/` will add a directory +`d` remove delete a file (will prompt for confirmation) +`D` trash trash a file via |trash| option +`r` rename rename a file +`` full_rename rename a file and omit the filename on input +`x` cut add/remove file/directory to cut clipboard +`c` copy add/remove file/directory to copy clipboard +`p` paste paste from clipboard; cut clipboard has precedence over copy; will prompt for confirmation +`y` copy_name copy name to system clipboard +`Y` copy_path copy relative path to system clipboard +`gy` copy_absolute_path copy absolute path to system clipboard +`[e` prev_diag_item go to next diagnostic item +`[c` prev_git_item go to next git item +`]e` next_diag_item go to prev diagnostic item +`]c` next_git_item go to prev git item +`-` dir_up navigate up to the parent directory of the current file/directory +`s` system_open open a file with default system application or a folder with default file manager, using |system_open| option +`f` live_filter live filter nodes dynamically based on regex matching. +`F` clear_live_filter clear live filter +`q` close close tree window +`W` collapse_all collapse the whole tree +`E` expand_all expand the whole tree, stopping after expanding |actions.expand_all.max_folder_discovery| folders; this might hang neovim for a while if running on a big folder +`S` search_node prompt the user to enter a path and then expands the tree to match the path +`.` run_file_command enter vim command mode with the file the cursor is on +`` toggle_file_info toggle a popup with file infos about the file under the cursor +`g?` toggle_help toggle help +`m` toggle_mark Toggle node in bookmarks +`bmv` bulk_move Move all bookmarked nodes into specified location > view.mappings.list = { -- BEGIN_DEFAULT_MAPPINGS @@ -1368,8 +1387,10 @@ DEFAULT MAPPINGS *nvim-tree-default-mappings { key = "", action = "preview" }, { key = "K", action = "first_sibling" }, { key = "J", action = "last_sibling" }, + { key = "C", action = "toggle_git_clean" }, { key = "I", action = "toggle_git_ignored" }, { key = "H", action = "toggle_dotfiles" }, + { key = "B", action = "toggle_no_buffer" }, { key = "U", action = "toggle_custom" }, { key = "R", action = "refresh" }, { key = "a", action = "create" }, diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 4992de68a53..e73a3344b67 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -11,6 +11,7 @@ local reloaders = require "nvim-tree.actions.reloaders.reloaders" local copy_paste = require "nvim-tree.actions.fs.copy-paste" local collapse_all = require "nvim-tree.actions.tree-modifiers.collapse-all" local git = require "nvim-tree.git" +local filters = require "nvim-tree.explorer.filters" local _config = {} @@ -353,6 +354,22 @@ local function setup_autocommands(opts) create_nvim_tree_autocmd("BufWritePost", { callback = reloaders.reload_explorer }) end + create_nvim_tree_autocmd("BufReadPost", { + callback = function() + if filters.config.filter_no_buffer then + reloaders.reload_explorer() + end + end, + }) + + create_nvim_tree_autocmd("BufUnload", { + callback = function(data) + if filters.config.filter_no_buffer then + reloaders.reload_explorer(nil, data.buf) + end + end, + }) + if not has_watchers and opts.git.enable then create_nvim_tree_autocmd("User", { pattern = { "FugitiveChanged", "NeogitStatusRefreshed" }, @@ -583,6 +600,8 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS }, filters = { dotfiles = false, + git_clean = false, + no_buffer = false, custom = {}, exclude = {}, }, diff --git a/lua/nvim-tree/actions/dispatch.lua b/lua/nvim-tree/actions/dispatch.lua index 44eff9834b0..e28fdb5c750 100644 --- a/lua/nvim-tree/actions/dispatch.lua +++ b/lua/nvim-tree/actions/dispatch.lua @@ -12,6 +12,8 @@ local Actions = { toggle_dotfiles = require("nvim-tree.actions.tree-modifiers.toggles").dotfiles, toggle_custom = require("nvim-tree.actions.tree-modifiers.toggles").custom, toggle_git_ignored = require("nvim-tree.actions.tree-modifiers.toggles").git_ignored, + toggle_git_clean = require("nvim-tree.actions.tree-modifiers.toggles").git_clean, + toggle_no_buffer = require("nvim-tree.actions.tree-modifiers.toggles").no_buffer, -- Filesystem operations copy_absolute_path = require("nvim-tree.actions.fs.copy-paste").copy_absolute_path, diff --git a/lua/nvim-tree/actions/finders/search-node.lua b/lua/nvim-tree/actions/finders/search-node.lua index 66fb5a3cfa5..23cb1c68543 100644 --- a/lua/nvim-tree/actions/finders/search-node.lua +++ b/lua/nvim-tree/actions/finders/search-node.lua @@ -14,6 +14,8 @@ local function search(search_dir, input_path) local function iter(dir) local realpath, path, name, stat, handle, _ + local filter_status = filters.prepare() + handle, _ = vim.loop.fs_scandir(dir) if not handle then return @@ -34,7 +36,7 @@ local function search(search_dir, input_path) break end - if not filters.should_ignore(path) then + if not filters.should_filter(path, filter_status) then if string.find(path, "/" .. input_path .. "$") then return path end diff --git a/lua/nvim-tree/actions/init.lua b/lua/nvim-tree/actions/init.lua index f42875f92ed..5764249c4d7 100644 --- a/lua/nvim-tree/actions/init.lua +++ b/lua/nvim-tree/actions/init.lua @@ -76,6 +76,11 @@ local DEFAULT_MAPPINGS = { action = "last_sibling", desc = "navigate to the last sibling of current file/directory", }, + { + key = "C", + action = "toggle_git_clean", + desc = "toggle visibility of git clean via |filters.git_clean| option", + }, { key = "I", action = "toggle_git_ignored", @@ -86,6 +91,11 @@ local DEFAULT_MAPPINGS = { action = "toggle_dotfiles", desc = "toggle visibility of dotfiles via |filters.dotfiles| option", }, + { + key = "B", + action = "toggle_no_buffer", + desc = "toggle visibility of files/folders hidden via |filters.no_buffer| option", + }, { key = "U", action = "toggle_custom", diff --git a/lua/nvim-tree/actions/reloaders/reloaders.lua b/lua/nvim-tree/actions/reloaders/reloaders.lua index c2b403a6954..7fca29068d1 100644 --- a/lua/nvim-tree/actions/reloaders/reloaders.lua +++ b/lua/nvim-tree/actions/reloaders/reloaders.lua @@ -6,13 +6,13 @@ local core = require "nvim-tree.core" local M = {} -local function refresh_nodes(node, projects) +local function refresh_nodes(node, projects, unloaded_bufnr) local cwd = node.cwd or node.link_to or node.absolute_path local project_root = git.get_project_root(cwd) - explorer_module.reload(node, projects[project_root] or {}) + explorer_module.reload(node, projects[project_root] or {}, unloaded_bufnr) for _, _node in ipairs(node.nodes) do if _node.nodes and _node.open then - refresh_nodes(_node, projects) + refresh_nodes(_node, projects, unloaded_bufnr) end end end @@ -33,14 +33,16 @@ function M.reload_node_status(parent_node, projects) end local event_running = false -function M.reload_explorer() +---@param _ table unused node passed by action +---@param unloaded_bufnr number optional bufnr recently unloaded via BufUnload event +function M.reload_explorer(_, unloaded_bufnr) if event_running or not core.get_explorer() or vim.v.exiting ~= vim.NIL then return end event_running = true local projects = git.reload() - refresh_nodes(core.get_explorer(), projects) + refresh_nodes(core.get_explorer(), projects, unloaded_bufnr) if view.is_visible() then renderer.draw() end diff --git a/lua/nvim-tree/actions/tree-modifiers/toggles.lua b/lua/nvim-tree/actions/tree-modifiers/toggles.lua index 63c9e07c66c..e56d1b37409 100644 --- a/lua/nvim-tree/actions/tree-modifiers/toggles.lua +++ b/lua/nvim-tree/actions/tree-modifiers/toggles.lua @@ -15,6 +15,16 @@ function M.git_ignored() return reloaders.reload_explorer() end +function M.git_clean() + filters.config.filter_git_clean = not filters.config.filter_git_clean + return reloaders.reload_explorer() +end + +function M.no_buffer() + filters.config.filter_no_buffer = not filters.config.filter_no_buffer + return reloaders.reload_explorer() +end + function M.dotfiles() filters.config.filter_dotfiles = not filters.config.filter_dotfiles return reloaders.reload_explorer() diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua index 5f93fc19db7..92c14dc45f2 100644 --- a/lua/nvim-tree/api.lua +++ b/lua/nvim-tree/api.lua @@ -38,6 +38,8 @@ Api.tree.search_node = require("nvim-tree.actions.finders.search-node").fn Api.tree.collapse_all = require("nvim-tree.actions.tree-modifiers.collapse-all").fn Api.tree.expand_all = inject_node(require("nvim-tree.actions.tree-modifiers.expand-all").fn) Api.tree.toggle_gitignore_filter = require("nvim-tree.actions.tree-modifiers.toggles").git_ignored +Api.tree.toggle_git_clean_filter = require("nvim-tree.actions.tree-modifiers.toggles").git_clean +Api.tree.toggle_no_buffer_filter = require("nvim-tree.actions.tree-modifiers.toggles").no_buffer Api.tree.toggle_custom_filter = require("nvim-tree.actions.tree-modifiers.toggles").custom Api.tree.toggle_hidden_filter = require("nvim-tree.actions.tree-modifiers.toggles").dotfiles Api.tree.toggle_help = require("nvim-tree.actions.tree-modifiers.toggles").help diff --git a/lua/nvim-tree/explorer/explore.lua b/lua/nvim-tree/explorer/explore.lua index 16c6fac81e4..da0803c8419 100644 --- a/lua/nvim-tree/explorer/explore.lua +++ b/lua/nvim-tree/explorer/explore.lua @@ -12,9 +12,10 @@ local function get_type_from(type_, cwd) return type_ or (vim.loop.fs_stat(cwd) or {}).type end -local function populate_children(handle, cwd, node, status) +local function populate_children(handle, cwd, node, git_status) local node_ignored = node.git_status == "!!" local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") + local filter_status = filters.prepare(git_status) while true do local name, t = vim.loop.fs_scandir_next(handle) if not name then @@ -23,11 +24,7 @@ local function populate_children(handle, cwd, node, status) local abs = utils.path_join { cwd, name } t = get_type_from(t, abs) - if - not filters.should_ignore(abs) - and not filters.should_ignore_git(abs, status.files) - and not nodes_by_path[abs] - then + if not filters.should_filter(abs, filter_status) and not nodes_by_path[abs] then local child = nil if t == "directory" and vim.loop.fs_access(abs, "R") then child = builders.folder(node, abs, name) @@ -42,7 +39,7 @@ local function populate_children(handle, cwd, node, status) if child then table.insert(node.nodes, child) nodes_by_path[child.absolute_path] = true - common.update_git_status(child, node_ignored, status) + common.update_git_status(child, node_ignored, git_status) end end end diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua index e26095f2ec3..c5701deaacd 100644 --- a/lua/nvim-tree/explorer/filters.lua +++ b/lua/nvim-tree/explorer/filters.lua @@ -14,26 +14,63 @@ local function is_excluded(path) return false end ----Check if the given path should be ignored. +---Check if the given path is git clean/ignored ---@param path string Absolute path +---@param git_status table from prepare ---@return boolean -function M.should_ignore(path) - local basename = utils.path_basename(path) +local function git(path, git_status) + if type(git_status) ~= "table" or type(git_status.files) ~= "table" or type(git_status.dirs) ~= "table" then + return false + end - if is_excluded(path) then + -- default status to clean + local status = git_status.files[path] or git_status.dirs[path] or " " + + -- filter ignored; overrides clean as they are effectively dirty + if M.config.filter_git_ignored and status == "!!" then + return true + end + + -- filter clean + if M.config.filter_git_clean and status == " " then + return true + end + + return false +end + +---Check if the given path has no listed buffer +---@param path string Absolute path +---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 } +---@param unloaded_bufnr number optional bufnr recently unloaded via BufUnload event +---@return boolean +local function buf(path, bufinfo, unloaded_bufnr) + if not M.config.filter_no_buffer or type(bufinfo) ~= "table" then return false end - if M.config.filter_dotfiles then - if basename:sub(1, 1) == "." then - return true + -- filter files with no open buffer and directories containing no open buffers + for _, b in ipairs(bufinfo) do + if b.name == path or b.name:find(path .. "/", 1, true) and b.bufnr ~= unloaded_bufnr then + return false end end + return true +end + +local function dotfile(path) + return M.config.filter_dotfiles and utils.path_basename(path):sub(1, 1) == "." +end + +local function custom(path) if not M.config.filter_custom then return false end + local basename = utils.path_basename(path) + + -- filter custom regexes local relpath = utils.path_relative(path, vim.loop.cwd()) for pat, _ in pairs(M.ignore_list) do if vim.fn.match(relpath, pat) ~= -1 or vim.fn.match(basename, pat) ~= -1 then @@ -51,10 +88,41 @@ function M.should_ignore(path) return false end -function M.should_ignore_git(path, status) - return M.config.filter_git_ignored - and (M.config.filter_git_ignored and status and status[path] == "!!") - and not is_excluded(path) +---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons. +---@param git_status table results of git.load_project_status(...) +---@param unloaded_bufnr number optional bufnr recently unloaded via BufUnload event +---@return table +--- git_status: reference +--- unloaded_bufnr: copy +--- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 } +function M.prepare(git_status, unloaded_bufnr) + local status = { + git_status = git_status or {}, + unloaded_bufnr = unloaded_bufnr, + bufinfo = {}, + } + + if M.config.filter_no_buffer then + status.bufinfo = vim.fn.getbufinfo { buflisted = 1 } + end + + return status +end + +---Check if the given path should be filtered. +---@param path string Absolute path +---@param status table from prepare +---@return boolean +function M.should_filter(path, status) + -- exclusions override all filters + if is_excluded(path) then + return false + end + + return git(path, status.git_status) + or buf(path, status.bufinfo, status.unloaded_bufnr) + or dotfile(path) + or custom(path) end function M.setup(opts) @@ -62,6 +130,8 @@ function M.setup(opts) filter_custom = true, filter_dotfiles = opts.filters.dotfiles, filter_git_ignored = opts.git.ignore, + filter_git_clean = opts.filters.git_clean, + filter_no_buffer = opts.filters.no_buffer, } M.ignore_list = {} diff --git a/lua/nvim-tree/explorer/reload.lua b/lua/nvim-tree/explorer/reload.lua index 315bf34b61c..8f1a3384153 100644 --- a/lua/nvim-tree/explorer/reload.lua +++ b/lua/nvim-tree/explorer/reload.lua @@ -34,7 +34,7 @@ local function update_parent_statuses(node, project, root) end end -function M.reload(node, status) +function M.reload(node, git_status, unloaded_bufnr) local cwd = node.link_to or node.absolute_path local handle = vim.loop.fs_scandir(cwd) if type(handle) == "string" then @@ -44,6 +44,8 @@ function M.reload(node, status) local ps = log.profile_start("reload %s", node.absolute_path) + local filter_status = filters.prepare(git_status, unloaded_bufnr) + if node.group_next then node.nodes = { node.group_next } node.group_next = nil @@ -71,7 +73,7 @@ function M.reload(node, status) local abs = utils.path_join { cwd, name } t = t or (fs_stat_cached(abs) or {}).type - if not filters.should_ignore(abs) and not filters.should_ignore_git(abs, status.files) then + if not filters.should_filter(abs, filter_status) then child_names[abs] = true -- Recreate node if type changes. @@ -112,7 +114,7 @@ function M.reload(node, status) end node.nodes = vim.tbl_map( - update_status(nodes_by_path, node_ignored, status), + update_status(nodes_by_path, node_ignored, git_status), vim.tbl_filter(function(n) if child_names[n.absolute_path] then return child_names[n.absolute_path] @@ -127,7 +129,7 @@ function M.reload(node, status) local child_folder_only = common.has_one_child_folder(node) and node.nodes[1] if M.config.group_empty and not is_root and child_folder_only then node.group_next = child_folder_only - local ns = M.reload(child_folder_only, status) + local ns = M.reload(child_folder_only, git_status) node.nodes = ns or {} log.profile_end(ps, "reload %s", node.absolute_path) return ns diff --git a/lua/nvim-tree/renderer/components/icons.lua b/lua/nvim-tree/renderer/components/icons.lua index 393d5ea29ec..3c5d27b82e1 100644 --- a/lua/nvim-tree/renderer/components/icons.lua +++ b/lua/nvim-tree/renderer/components/icons.lua @@ -85,7 +85,7 @@ end function M.setup(opts) M.config = opts.renderer.icons - M.devicons = pcall(require, "nvim-web-devicons") and require "nvim-web-devicons" + M.devicons = pcall(require, "nvim-web-devicons") and require "nvim-web-devicons" or nil end return M