diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 34caa89e72c..35429c937c7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,6 +9,10 @@ assignees: '' **Is this a question?** Please start a new [Q&A discussion](https://github.com/nvim-tree/nvim-tree.lua/discussions/new) instead of raising a feature request. +**Can this functionality be implemented utilising API?** +nvim-tree exposes extensive API (see `:h nvim-tree-api`). Can it be used to achieve your goal? Is there a missing API that would make it possible? +Given stable status of nvim-tree it's preferred to add new API than new functionality. + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/README.md b/README.md index e1ff7a74c7d..cf778b4a584 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Take a look at the [wiki](https://github.com/nvim-tree/nvim-tree.lua/wiki) for S [neovim >=0.7.0](https://github.com/neovim/neovim/wiki/Installing-Neovim) -[nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) is optional and used to display file icons. It requires a [patched font](https://www.nerdfonts.com/). +[nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) is optional and used to display file icons. It requires a [patched font](https://www.nerdfonts.com/). Your terminal emulator must be configured to use that font, usually "Hack Nerd Font" ## Install diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index db6dc8d750a..32ed5a6a02d 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -31,7 +31,9 @@ Features File Icons - https://github.com/nvim-tree/nvim-web-devicons is optional and used to display file icons. It requires a patched font: https://www.nerdfonts.com + https://github.com/nvim-tree/nvim-web-devicons is optional and used to display file icons. + It requires a patched font: https://www.nerdfonts.com + Your terminal emulator must be configured to use that font, usually "Hack Nerd Font"  should look like an open folder. @@ -169,7 +171,6 @@ Subsequent calls to setup will replace the previous configuration. > require("nvim-tree").setup { -- BEGIN_DEFAULT_OPTS auto_reload_on_write = true, - create_in_closed_folder = false, disable_netrw = false, hijack_cursor = false, hijack_netrw = true, @@ -290,10 +291,11 @@ Subsequent calls to setup will replace the previous configuration. diagnostics = { enable = false, show_on_dirs = false, + show_on_open_dirs = true, debounce_delay = 50, severity = { min = vim.diagnostic.severity.HINT, - max = vim.diagnostic.severity.ERROR + max = vim.diagnostic.severity.ERROR, }, icons = { hint = "", @@ -304,6 +306,8 @@ Subsequent calls to setup will replace the previous configuration. }, filters = { dotfiles = false, + git_clean = false, + no_buffer = false, custom = {}, exclude = {}, }, @@ -316,6 +320,7 @@ Subsequent calls to setup will replace the previous configuration. enable = true, ignore = true, show_on_dirs = true, + show_on_open_dirs = true, timeout = 400, }, actions = { @@ -430,11 +435,6 @@ in some scenarios (eg using vim startify). Reloads the explorer every time a buffer is written to. Type: `boolean`, Default: `true` -*nvim-tree.create_in_closed_folder* -Creating a file when the cursor is on a closed folder will set the -path to be inside the closed folder, otherwise the parent folder. - Type: `boolean`, Default: `false` - *nvim-tree.sort_by* Changes how files within the same directory are sorted. Can be one of `name`, `case_sensitive`, `modification_time`, `extension` or a @@ -555,6 +555,11 @@ Show LSP and COC diagnostics in the signcolumn Show diagnostic icons on parent directories. Type: `boolean`, Default: `false` + *nvim-tree.diagnostics.show_on_open_dirs* + Show diagnostics icons on directories that are open. + Only relevant when `diagnostics.show_on_dirs` is `true`. + Type: `boolean`, Default: `true` + *nvim-tree.diagnostics.icons* Icons for diagnostic severity. Type: `table`, Default: `{ hint = "", info = "", warning = "", error = "" }` @@ -586,6 +591,11 @@ Git integration with icons and colors. Show status icons of children when directory itself has no status icon. Type: `boolean`, Default: `true` + *nvim-tree.git.show_on_open_dirs* + Show status icons on directories that are open. + Only relevant when `git.show_on_dirs` is `true`. + Type: `boolean`, Default: `true` + *nvim-tree.git.timeout* Kills the git process after some time if it takes too long. Type: `number`, Default: `400` (ms) @@ -627,16 +637,14 @@ Function ran when creating the nvim-tree buffer. This can be used to attach keybindings to the tree buffer. When on_attach is not a function, |nvim-tree.view.mappings| will be used. Type: `function(bufnr)`, Default: `"disable"` + e.g. > + local api = require('nvim-tree.api') - Example: > - local Api = require('nvim-tree.api') - local Lib = require('nvim-tree.lib') - - local my_on_attach = function(bufnr) - vim.keymap.set('n', '?', Api.tree.toggle_help, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Help' }) - vim.keymap.set('n', 'h', Api.tree.toggle_help, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Help' }) + local function on_attach(bufnr) + vim.keymap.set('n', '?', api.tree.toggle_help, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Help' }) + vim.keymap.set('n', 'h', api.tree.toggle_help, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Help' }) vim.keymap.set("n", "", function() - local node = Lib.get_node_at_cursor() + local node = api.tree.get_node_under_cursor() print(node.absolute_path) end, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = "my description" }) end @@ -704,6 +712,7 @@ Window / buffer setup. *nvim-tree.view.mappings.custom_only* Will use only the provided user mappings and not the default otherwise, extends the default mappings with the provided user mappings. + Overrides |nvim-tree.remove_keymaps| Type: `boolean`, Default: `false` *nvim-tree.view.mappings.list* @@ -888,6 +897,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|. @@ -1142,15 +1164,15 @@ A good functionality to enable is |nvim-tree.hijack_directories|. Nvim-tree's public API can be used to access features. > - local nt_api = require("nvim-tree.api") - - nt_api.tree.toggle() +e.g. > + local api = require("nvim-tree.api") + api.tree.toggle() < This module exposes stable functionalities, it is advised to use this in order to avoid breaking configurations due to internal breaking changes. The api is separated in multiple modules, which can be accessed with -`require("nvim-tree.api").moduleName.functionality`. +`api..` Functions that needs a tree node parameter are exposed with an abstraction that injects the node from the cursor position in the tree when calling @@ -1173,6 +1195,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 @@ -1294,7 +1318,7 @@ DEFAULT MAPPINGS *nvim-tree-default-mappings `O` Open: No Window Picker Open file with no window picker. `` CD cd in the directory under the cursor. `<2-RightMouse>` -`` Open: Vertical Split Open file in a vertical split. +`` Open: Vertical Split Open file in a vertical split. `` Open: Horizontal Split Open file in a horizontal split. `` Open: New Tab Open file in a new tab. `<` Previous Sibling Navigate to the previous sibling. @@ -1348,7 +1372,7 @@ DEFAULT MAPPINGS *nvim-tree-default-mappings vim.keymap.set('n', 'O', Api.node.open.no_window_picker, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Open: No Window Picker', }) vim.keymap.set('n', '', Api.tree.change_root_to_node, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'CD', }) vim.keymap.set('n', '<2-RightMouse>', Api.tree.change_root_to_node, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'CD', }) - vim.keymap.set('n', '', Api.node.open.vertical, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Open: Vertical Split', }) + vim.keymap.set('n', '', Api.node.open.vertical, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Open: Vertical Split', }) vim.keymap.set('n', '', Api.node.open.horizontal, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Open: Horizontal Split', }) vim.keymap.set('n', '', Api.node.open.tab, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Open: New Tab', }) vim.keymap.set('n', '<', Api.node.navigate.sibling.prev, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = 'Previous Sibling', }) @@ -1416,6 +1440,8 @@ NvimTreeSymlink NvimTreeFolderName (Directory) NvimTreeRootFolder NvimTreeFolderIcon +NvimTreeOpenedFolderIcon (NvimTreeFolderIcon) +NvimTreeClosedFolderIcon (NvimTreeFolderIcon) NvimTreeFileIcon NvimTreeEmptyFolderName (Directory) NvimTreeOpenedFolderName (Directory) @@ -1487,27 +1513,19 @@ to |nvim_tree_registering_handlers| for more information. |nvim_tree_registering_handlers| -Handlers are registered by calling the `events.subscribe` function available in the -`require("nvim-tree.api")` module. +Handlers are registered by calling |nvim-tree-api| `events.subscribe` +function with an `events.Event` kind. -For example, registering a handler for when a node is renamed is done like this: -> - local api = require('nvim-tree.api') +e.g. handler for node renamed: > + local api = require("nvim-tree.api") local Event = api.events.Event api.events.subscribe(Event.NodeRenamed, function(data) print("Node renamed from " .. data.old_name .. " to " .. data.new_name) end) < - |nvim_tree_events_kind| -You can access the event enum with: -> - local Event = require('nvim-tree.api').events.Event -< -Here is the list of available variant of this enum: - - Event.Ready When NvimTree has been initialized • Note: Handler takes no parameter. diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 348d47bf4a4..63448afb87c 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 = {} @@ -351,6 +352,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" }, @@ -445,7 +462,6 @@ end local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS auto_reload_on_write = true, - create_in_closed_folder = false, disable_netrw = false, hijack_cursor = false, hijack_netrw = true, @@ -566,6 +582,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS diagnostics = { enable = false, show_on_dirs = false, + show_on_open_dirs = true, debounce_delay = 50, severity = { min = vim.diagnostic.severity.HINT, @@ -580,6 +597,8 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS }, filters = { dotfiles = false, + git_clean = false, + no_buffer = false, custom = {}, exclude = {}, }, @@ -592,6 +611,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS enable = true, ignore = true, show_on_dirs = true, + show_on_open_dirs = true, timeout = 400, }, actions = { @@ -757,7 +777,7 @@ function M.setup(conf) log.line("config", "default config + user") log.raw("config", "%s\n", vim.inspect(opts)) - legacy.move_mappings_to_keymap(opts) + legacy.generate_legacy_on_attach(opts) require("nvim-tree.actions").setup(opts) require("nvim-tree.keymap").setup(opts) 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/fs/copy-paste.lua b/lua/nvim-tree/actions/fs/copy-paste.lua index c5cd5e2acd3..ffcbd4133f9 100644 --- a/lua/nvim-tree/actions/fs/copy-paste.lua +++ b/lua/nvim-tree/actions/fs/copy-paste.lua @@ -2,6 +2,7 @@ local lib = require "nvim-tree.lib" local log = require "nvim-tree.log" local utils = require "nvim-tree.utils" local core = require "nvim-tree.core" +local events = require "nvim-tree.events" local notify = require "nvim-tree.notify" local M = {} @@ -160,11 +161,8 @@ local function do_paste(node, action_type, action_fn) return end local is_dir = stats and stats.type == "directory" - if not is_dir then destination = vim.fn.fnamemodify(destination, ":p:h") - elseif not node.open then - destination = vim.fn.fnamemodify(destination, ":p:h:h") end for _, _node in ipairs(clip) do @@ -192,6 +190,7 @@ local function do_cut(source, destination) return false, errmsg end utils.rename_loaded_buffers(source, destination) + events._dispatch_node_renamed(source, destination) return true end diff --git a/lua/nvim-tree/actions/fs/create-file.lua b/lua/nvim-tree/actions/fs/create-file.lua index 35ec267d77d..c970eeddab5 100644 --- a/lua/nvim-tree/actions/fs/create-file.lua +++ b/lua/nvim-tree/actions/fs/create-file.lua @@ -42,8 +42,7 @@ local function get_num_nodes(iter) end local function get_containing_folder(node) - local is_open = M.create_in_closed_folder or node.open - if node.nodes ~= nil and is_open then + if node.nodes ~= nil then return utils.path_add_trailing(node.absolute_path) end local node_name_size = #(node.name or "") @@ -113,7 +112,6 @@ function M.fn(node) end function M.setup(opts) - M.create_in_closed_folder = opts.create_in_closed_folder M.enable_reload = not opts.filesystem_watchers.enable end diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua index 3eb74b807a2..b5646193346 100644 --- a/lua/nvim-tree/actions/moves/item.lua +++ b/lua/nvim-tree/actions/moves/item.lua @@ -2,6 +2,7 @@ local utils = require "nvim-tree.utils" local view = require "nvim-tree.view" local core = require "nvim-tree.core" local lib = require "nvim-tree.lib" +local explorer_common = require "nvim-tree.explorer.common" local M = {} @@ -14,7 +15,7 @@ function M.fn(where, what) for line, node in pairs(nodes_by_line) do local valid = false if what == "git" then - valid = node.git_status ~= nil + valid = explorer_common.shows_git_status(node) elseif what == "diag" then valid = node.diag_status ~= nil end 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 91dbc3e03ed..13ef4b3e743 100644 --- a/lua/nvim-tree/api.lua +++ b/lua/nvim-tree/api.lua @@ -46,6 +46,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/colors.lua b/lua/nvim-tree/colors.lua index 6e6a98d1807..33af05c0e5e 100644 --- a/lua/nvim-tree/colors.lua +++ b/lua/nvim-tree/colors.lua @@ -61,6 +61,8 @@ local function get_links() FolderName = "Directory", EmptyFolderName = "Directory", OpenedFolderName = "Directory", + OpenedFolderIcon = "NvimTreeFolderIcon", + ClosedFolderIcon = "NvimTreeFolderIcon", Normal = "Normal", NormalNC = "NvimTreeNormal", EndOfBuffer = "EndOfBuffer", diff --git a/lua/nvim-tree/diagnostics.lua b/lua/nvim-tree/diagnostics.lua index 5337c2c1ffa..3771779def3 100644 --- a/lua/nvim-tree/diagnostics.lua +++ b/lua/nvim-tree/diagnostics.lua @@ -114,7 +114,7 @@ function M.update() for line, node in pairs(nodes_by_line) do local nodepath = utils.canonical_path(node.absolute_path) log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) - if M.show_on_dirs and vim.startswith(bufpath, nodepath) then + if M.show_on_dirs and vim.startswith(bufpath, nodepath) and (not node.open or M.show_on_open_dirs) then log.line("diagnostics", " matched fold node '%s'", node.absolute_path) node.diag_status = severity add_sign(line, severity) @@ -147,6 +147,7 @@ function M.setup(opts) end M.show_on_dirs = opts.diagnostics.show_on_dirs + M.show_on_open_dirs = opts.diagnostics.show_on_open_dirs vim.fn.sign_define(sign_names[1][1], { text = opts.diagnostics.icons.error, texthl = sign_names[1][2] }) vim.fn.sign_define(sign_names[2][1], { text = opts.diagnostics.icons.warning, texthl = sign_names[2][2] }) vim.fn.sign_define(sign_names[3][1], { text = opts.diagnostics.icons.info, texthl = sign_names[3][2] }) diff --git a/lua/nvim-tree/explorer/common.lua b/lua/nvim-tree/explorer/common.lua index c3bed863e1a..71eae8af608 100644 --- a/lua/nvim-tree/explorer/common.lua +++ b/lua/nvim-tree/explorer/common.lua @@ -10,9 +10,7 @@ local function get_dir_git_status(parent_ignored, status, absolute_path) return file_status end - if M.config.git.show_on_dirs then - return status.dirs and status.dirs[absolute_path] - end + return status.dirs and status.dirs[absolute_path] end local function get_git_status(parent_ignored, status, absolute_path) @@ -41,6 +39,22 @@ function M.update_git_status(node, parent_ignored, status) end end +function M.shows_git_status(node) + if not node.git_status then + -- status doesn't exist + return false + elseif not node.nodes then + -- status exist and is a file + return true + elseif not node.open then + -- status exist, is a closed dir + return M.config.git.show_on_dirs + else + -- status exist, is a open dir + return M.config.git.show_on_dirs and M.config.git.show_on_open_dirs + end +end + function M.node_destroy(node) if not node then return 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/git/runner.lua b/lua/nvim-tree/git/runner.lua index 686b5f80bd5..a03c72e4bca 100644 --- a/lua/nvim-tree/git/runner.lua +++ b/lua/nvim-tree/git/runner.lua @@ -4,10 +4,7 @@ local utils = require "nvim-tree.utils" local Runner = {} Runner.__index = Runner -function Runner:_parse_status_output(line) - local status = line:sub(1, 2) - -- removing `"` when git is returning special file status containing spaces - local path = line:sub(4, -2):gsub('^"', ""):gsub('"$', "") +function Runner:_parse_status_output(status, path) -- replacing slashes if on windows if vim.fn.has "win32" == 1 then path = path:gsub("/", "\\") @@ -15,15 +12,26 @@ function Runner:_parse_status_output(line) if #status > 0 and #path > 0 then self.output[utils.path_remove_trailing(utils.path_join { self.project_root, path })] = status end - return #line end function Runner:_handle_incoming_data(prev_output, incoming) if incoming and utils.str_find(incoming, "\n") then local prev = prev_output .. incoming local i = 1 + local skip_next_line = false for line in prev:gmatch "[^\n]*\n" do - i = i + self:_parse_status_output(line) + if skip_next_line then + skip_next_line = false + else + local status = line:sub(1, 2) + local path = line:sub(4, -2) + if utils.str_find(status, "R") then + -- skip next line if it is a rename entry + skip_next_line = true + end + self:_parse_status_output(status, path) + end + i = i + #line end return prev:sub(i, -1) @@ -44,7 +52,7 @@ function Runner:_getopts(stdout_handle, stderr_handle) local untracked = self.list_untracked and "-u" or nil local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no" return { - args = { "--no-optional-locks", "status", "--porcelain=v1", ignored, untracked, self.path }, + args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path }, cwd = self.project_root, stdio = { nil, stdout_handle, stderr_handle }, } @@ -106,6 +114,9 @@ function Runner:_run_git_job() if err then return end + if data then + data = data:gsub("%z", "\n") + end self:_log_raw_output(data) output_leftover = self:_handle_incoming_data(output_leftover, data) end @@ -122,6 +133,7 @@ function Runner:_wait() local function is_done() return self.rc ~= nil end + while not vim.wait(30, is_done) do end end diff --git a/lua/nvim-tree/keymap.lua b/lua/nvim-tree/keymap.lua index fa1ca95be3e..642e97ef72b 100644 --- a/lua/nvim-tree/keymap.lua +++ b/lua/nvim-tree/keymap.lua @@ -41,8 +41,7 @@ local DEFAULT_KEYMAPS = { legacy_action = "cd", }, { - -- key = "", - key = "", + key = "", callback = Api.node.open.vertical, desc = { long = "Open file in a vertical split.", @@ -422,10 +421,10 @@ local DEFAULT_KEYMAPS = { } -- END_DEFAULT_KEYMAPS --- TODO fuzzy filtering of keys e.g. "" <-> "" to prevent two mappings being created -function M.set_keymaps(bufnr) +function M.on_attach_default(bufnr) local opts = { noremap = true, silent = true, nowait = true, buffer = bufnr } - for _, km in ipairs(M.keymaps) do + + for _, km in ipairs(M.DEFAULT_KEYMAPS) do local keys = type(km.key) == "table" and km.key or { km.key } for _, key in ipairs(keys) do opts.desc = km.desc.short @@ -434,46 +433,16 @@ function M.set_keymaps(bufnr) end end -local function filter_default_mappings(keys_to_disable) - local new_map = {} - for _, m in pairs(DEFAULT_KEYMAPS) do - local keys = type(m.key) == "table" and m.key or { m.key } - local reminding_keys = {} - for _, key in pairs(keys) do - local found = false - for _, key_to_disable in pairs(keys_to_disable) do - if key_to_disable == key then - found = true - break - end - end - if not found then - table.insert(reminding_keys, key) - end - end - if #reminding_keys > 0 then - local map = vim.deepcopy(m) - map.key = reminding_keys - table.insert(new_map, map) - end - end - return new_map -end - -local function get_keymaps(keys_to_disable) - if keys_to_disable == true then - return {} +function M.setup(opts) + if type(opts.on_attach) == "function" then + M.on_attach = opts.on_attach + else + M.on_attach = M.on_attach_default end - if type(keys_to_disable) == "table" and #keys_to_disable > 0 then - return filter_default_mappings(keys_to_disable) + if type(opts.remove_keymaps) == "table" then + M.remove_keys = opts.remove_keymaps end - - return DEFAULT_KEYMAPS -end - -function M.setup(opts) - M.keymaps = get_keymaps(opts.remove_keymaps) end M.DEFAULT_KEYMAPS = DEFAULT_KEYMAPS diff --git a/lua/nvim-tree/legacy.lua b/lua/nvim-tree/legacy.lua index 4fdef7364ed..e23a8cb58fc 100644 --- a/lua/nvim-tree/legacy.lua +++ b/lua/nvim-tree/legacy.lua @@ -1,62 +1,62 @@ local utils = require "nvim-tree.utils" local notify = require "nvim-tree.notify" local open_file = require "nvim-tree.actions.node.open-file" +local keymap = require "nvim-tree.keymap" local log = require "nvim-tree.log" -local DEFAULT_KEYMAPS = require("nvim-tree.keymap").DEFAULT_KEYMAPS - local M = { on_attach_lua = "", } -- BEGIN_LEGACY_CALLBACKS local LEGACY_CALLBACKS = { - edit = "Api.node.open.edit", - edit_in_place = "Api.node.open.replace_tree_buffer", - edit_no_picker = "Api.node.open.no_window_picker", - cd = "Api.tree.change_root_to_node", - vsplit = "Api.node.open.vertical", - split = "Api.node.open.horizontal", - tabnew = "Api.node.open.tab", - prev_sibling = "Api.node.navigate.sibling.prev", - next_sibling = "Api.node.navigate.sibling.next", - parent_node = "Api.node.navigate.parent", - close_node = "Api.node.navigate.parent_close", - preview = "Api.node.open.preview", - first_sibling = "Api.node.navigate.sibling.first", - last_sibling = "Api.node.navigate.sibling.last", - toggle_git_ignored = "Api.tree.toggle_gitignore_filter", - toggle_dotfiles = "Api.tree.toggle_hidden_filter", - toggle_custom = "Api.tree.toggle_custom_filter", - refresh = "Api.tree.reload", - create = "Api.fs.create", - remove = "Api.fs.remove", - trash = "Api.fs.trash", - rename = "Api.fs.rename", - full_rename = "Api.fs.rename_sub", - cut = "Api.fs.cut", - copy = "Api.fs.copy.node", - paste = "Api.fs.paste", - copy_name = "Api.fs.copy.filename", - copy_path = "Api.fs.copy.relative_path", - copy_absolute_path = "Api.fs.copy.absolute_path", - next_diag_item = "Api.node.navigate.diagnostics.next", - next_git_item = "Api.node.navigate.git.next", - prev_diag_item = "Api.node.navigate.diagnostics.prev", - prev_git_item = "Api.node.navigate.git.prev", - dir_up = "Api.tree.change_root_to_parent", - system_open = "Api.node.run.system", - live_filter = "Api.live_filter.start", - clear_live_filter = "Api.live_filter.clear", - close = "Api.tree.close", - collapse_all = "Api.tree.collapse_all", - expand_all = "Api.tree.expand_all", - search_node = "Api.tree.search_node", - run_file_command = "Api.node.run.cmd", - toggle_file_info = "Api.node.show_info_popup", - toggle_help = "Api.tree.toggle_help", - toggle_mark = "Api.marks.toggle", - bulk_move = "Api.marks.bulk.move", + -- TODO sync + edit = "api.node.open.edit", + edit_in_place = "api.node.open.replace_tree_buffer", + edit_no_picker = "api.node.open.no_window_picker", + cd = "api.tree.change_root_to_node", + vsplit = "api.node.open.vertical", + split = "api.node.open.horizontal", + tabnew = "api.node.open.tab", + prev_sibling = "api.node.navigate.sibling.prev", + next_sibling = "api.node.navigate.sibling.next", + parent_node = "api.node.navigate.parent", + close_node = "api.node.navigate.parent_close", + preview = "api.node.open.preview", + first_sibling = "api.node.navigate.sibling.first", + last_sibling = "api.node.navigate.sibling.last", + toggle_git_ignored = "api.tree.toggle_gitignore_filter", + toggle_dotfiles = "api.tree.toggle_hidden_filter", + toggle_custom = "api.tree.toggle_custom_filter", + refresh = "api.tree.reload", + create = "api.fs.create", + remove = "api.fs.remove", + trash = "api.fs.trash", + rename = "api.fs.rename", + full_rename = "api.fs.rename_sub", + cut = "api.fs.cut", + copy = "api.fs.copy.node", + paste = "api.fs.paste", + copy_name = "api.fs.copy.filename", + copy_path = "api.fs.copy.relative_path", + copy_absolute_path = "api.fs.copy.absolute_path", + next_diag_item = "api.node.navigate.diagnostics.next", + next_git_item = "api.node.navigate.git.next", + prev_diag_item = "api.node.navigate.diagnostics.prev", + prev_git_item = "api.node.navigate.git.prev", + dir_up = "api.tree.change_root_to_parent", + system_open = "api.node.run.system", + live_filter = "api.live_filter.start", + clear_live_filter = "api.live_filter.clear", + close = "api.tree.close", + collapse_all = "api.tree.collapse_all", + expand_all = "api.tree.expand_all", + search_node = "api.tree.search_node", + run_file_command = "api.node.run.cmd", + toggle_file_info = "api.node.show_info_popup", + toggle_help = "api.tree.toggle_help", + toggle_mark = "api.marks.toggle", + bulk_move = "api.marks.bulk.move", } -- END_LEGACY_CALLBACKS @@ -325,12 +325,6 @@ local g_migrations = { o.respect_buf_cwd = vim.g.nvim_tree_respect_buf_cwd == 1 end end, - - nvim_tree_create_in_closed_folder = function(o) - if o.create_in_closed_folder == nil then - o.create_in_closed_folder = vim.g.nvim_tree_create_in_closed_folder == 1 - end - end, } local function refactored(opts) @@ -366,16 +360,21 @@ local function removed(opts) notify.warn "focus_empty_on_setup has been removed and will be replaced by a new startup configuration. Please remove this option. See https://bit.ly/3yJch2T" opts.focus_empty_on_setup = nil end + + if opts.create_in_closed_folder then + notify.warn "create_in_closed_folder has been removed and is now the default behaviour. You may use api.fs.create to add a file under your desired node." + end + opts.create_in_closed_folder = nil end +-- TODO pcall vim.keymap.del("n", key, opts) for action = "" and remove_keymaps local function build_on_attach(call_list) if #call_list == 0 then return nil end M.on_attach_lua = [[ -local Api = require('nvim-tree.api') -local Lib = require('nvim-tree.lib') +local api = require('nvim-tree.api') local on_attach = function(bufnr) ]] @@ -384,7 +383,7 @@ local on_attach = function(bufnr) local vim_keymap_set if el.action_cb then vim_keymap_set = string.format( - 'vim.keymap.set("n", "%s", function()\n local node = Lib.get_node_at_cursor()\n -- my code\n end, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = "my description" })', + 'vim.keymap.set("n", "%s", function()\n local node = api.tree.get_node_under_cursor()\n -- my code\n end, { buffer = bufnr, noremap = true, silent = true, nowait = true, desc = "my description" })', el.key ) elseif el.keymap then @@ -404,10 +403,11 @@ local on_attach = function(bufnr) M.on_attach_lua = string.format("%send\n", M.on_attach_lua) return function(bufnr) + keymap.on_attach_default(bufnr) for _, el in pairs(call_list) do if el.action_cb then vim.keymap.set(el.mode or "n", el.key, function() - el.action_cb(require("nvim-tree.lib").get_node_at_cursor()) + el.action_cb(require("nvim-tree.api").tree.get_node_under_cursor()) end, { buffer = bufnr, remap = false, silent = true }) elseif el.keymap then vim.keymap.set( @@ -421,42 +421,52 @@ local on_attach = function(bufnr) end end -function M.move_mappings_to_keymap(opts) +function M.generate_legacy_on_attach(opts) if type(opts.on_attach) ~= "function" and opts.view and opts.view.mappings then - local custom_only, list = opts.view.mappings.custom_only, opts.view.mappings.list + local list = opts.view.mappings.list log.line("config", "generating on_attach for %d legacy view.mappings.list:", #list) - if custom_only then + + if opts.view.mappings.custom_only then opts.remove_keymaps = true opts.view.mappings.custom_only = nil end + if list then - local keymap_by_legacy_action = utils.key_by(DEFAULT_KEYMAPS, "legacy_action") - if not custom_only then - opts.remove_keymaps = {} - end + local keymap_by_legacy_action = utils.key_by(keymap.DEFAULT_KEYMAPS, "legacy_action") local call_list = {} + + for _, km in ipairs(keymap.DEFAULT_KEYMAPS) do + local keys = type(km.key) == "table" and km.key or { km.key } + for _, k in ipairs(keys) do + table.insert(call_list, { mode = "n", key = k, keymap = km }) + end + end + for _, map in pairs(list) do local keys = type(map.key) == "table" and map.key or { map.key } local mode = map.mode or "n" local action_cb - local keymap + local km if map.action ~= "" then if map.action_cb then action_cb = map.action_cb elseif keymap_by_legacy_action[map.action] then - keymap = keymap_by_legacy_action[map.action] + km = keymap_by_legacy_action[map.action] end end for _, k in pairs(keys) do - if not custom_only and not vim.tbl_contains(opts.remove_keymaps, k) then + if map.action == "" and opts.remove_keymaps ~= true then + if type(opts.remove_keymaps) ~= "table" then + opts.remove_keymaps = {} + end table.insert(opts.remove_keymaps, k) end if action_cb then table.insert(call_list, { mode = mode, key = k, action_cb = action_cb }) - elseif keymap then - table.insert(call_list, { mode = mode, key = k, keymap = keymap }) + elseif km then + table.insert(call_list, { mode = mode, key = k, keymap = km }) end end end diff --git a/lua/nvim-tree/renderer/builder.lua b/lua/nvim-tree/renderer/builder.lua index bccb93a370a..1a63900e989 100644 --- a/lua/nvim-tree/renderer/builder.lua +++ b/lua/nvim-tree/renderer/builder.lua @@ -127,7 +127,11 @@ function Builder:_build_folder(node, padding, git_hl, git_icons_tbl) self:_insert_line(line) if #icon > 0 then - self:_insert_highlight("NvimTreeFolderIcon", offset, offset + #icon) + if node.open then + self:_insert_highlight("NvimTreeOpenedFolderIcon", offset, offset + #icon) + else + self:_insert_highlight("NvimTreeClosedFolderIcon", offset, offset + #icon) + end end local foldername_hl = "NvimTreeFolderName" diff --git a/lua/nvim-tree/renderer/components/git.lua b/lua/nvim-tree/renderer/components/git.lua index 644e06c353b..fae22803f30 100644 --- a/lua/nvim-tree/renderer/components/git.lua +++ b/lua/nvim-tree/renderer/components/git.lua @@ -1,4 +1,5 @@ local notify = require "nvim-tree.notify" +local explorer_common = require "nvim-tree.explorer.common" local M = { SIGN_GROUP = "NvimTreeGitSigns", @@ -76,11 +77,11 @@ local function warn_status(git_status) end local function get_icons_(node) - local git_status = node.git_status - if not git_status then + if not explorer_common.shows_git_status(node) then return nil end + local git_status = node.git_status local icons = M.git_icons[git_status] if not icons then if not M.config.highlight_git then @@ -137,7 +138,7 @@ end local function get_highlight_(node) local git_status = node.git_status - if not git_status then + if not explorer_common.shows_git_status(node) then return end @@ -162,6 +163,8 @@ function M.setup(opts) else M.get_highlight = nil_ end + + M.git_show_on_open_dirs = opts.git.show_on_open_dirs end return M 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 diff --git a/lua/nvim-tree/view.lua b/lua/nvim-tree/view.lua index 97d78e1c1a2..3e7dca9a008 100644 --- a/lua/nvim-tree/view.lua +++ b/lua/nvim-tree/view.lua @@ -27,6 +27,7 @@ M.View = { foldmethod = "manual", foldcolumn = "0", cursorcolumn = false, + cursorline = true, cursorlineopt = "both", colorcolumn = "0", wrap = false, @@ -93,10 +94,7 @@ local function create_buffer(bufnr) vim.bo[M.get_bufnr()][option] = value end - require("nvim-tree.keymap").set_keymaps(M.get_bufnr()) - if type(M.on_attach) == "function" then - M.on_attach(M.get_bufnr()) - end + require("nvim-tree.keymap").on_attach(M.get_bufnr()) end local function get_size() @@ -491,7 +489,6 @@ function M.setup(opts) M.View.winopts.relativenumber = options.relativenumber M.View.winopts.signcolumn = options.signcolumn M.View.float = options.float - M.on_attach = opts.on_attach end return M