├── doc └── .gitignore ├── .github ├── FUNDING.yml ├── screenshot.png ├── example.help.png ├── example.tree.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── workflows │ └── ci.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── nvt-min.lua │ └── bug_report.yml ├── .hooks └── pre-commit.sh ├── scripts ├── setup-hooks.sh ├── update-help.sh └── generate_default_mappings.lua ├── .editorconfig ├── .stylua.toml ├── .luacheckrc ├── lua └── nvim-tree │ ├── config.lua │ ├── actions │ ├── root │ │ ├── dir-up.lua │ │ └── change-dir.lua │ ├── node │ │ ├── run-command.lua │ │ ├── system-open.lua │ │ ├── file-popup.lua │ │ └── open-file.lua │ ├── tree-modifiers │ │ ├── toggles.lua │ │ ├── collapse-all.lua │ │ └── expand-all.lua │ ├── moves │ │ ├── parent.lua │ │ ├── sibling.lua │ │ └── item.lua │ ├── fs │ │ ├── rename-file.lua │ │ ├── remove-file.lua │ │ ├── trash.lua │ │ ├── create-file.lua │ │ └── copy-paste.lua │ ├── reloaders │ │ └── reloaders.lua │ ├── finders │ │ ├── find-file.lua │ │ └── search-node.lua │ ├── dispatch.lua │ └── init.lua │ ├── core.lua │ ├── marks │ ├── bulk-move.lua │ ├── init.lua │ └── navigation.lua │ ├── iterators │ └── node-iterator.lua │ ├── explorer │ ├── init.lua │ ├── common.lua │ ├── filters.lua │ ├── watch.lua │ ├── explore.lua │ ├── node-builders.lua │ ├── reload.lua │ └── sorters.lua │ ├── renderer │ ├── help.lua │ ├── components │ │ ├── full-name.lua │ │ ├── icons.lua │ │ ├── padding.lua │ │ └── git.lua │ ├── init.lua │ └── builder.lua │ ├── log.lua │ ├── git │ ├── utils.lua │ ├── runner.lua │ └── init.lua │ ├── events.lua │ ├── colors.lua │ ├── watcher.lua │ ├── live-filter.lua │ ├── lib.lua │ ├── diagnostics.lua │ ├── api.lua │ ├── keymap.lua │ └── legacy.lua ├── .luarc.json ├── LICENSE ├── CONTRIBUTING.md └── README.md /doc/.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kyazdani42 4 | -------------------------------------------------------------------------------- /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillianFuks/nvim-tree.lua/master/.github/screenshot.png -------------------------------------------------------------------------------- /.hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | stylua . --check || exit 1 4 | luacheck . || exit 1 5 | -------------------------------------------------------------------------------- /.github/example.help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillianFuks/nvim-tree.lua/master/.github/example.help.png -------------------------------------------------------------------------------- /.github/example.tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillianFuks/nvim-tree.lua/master/.github/example.tree.png -------------------------------------------------------------------------------- /.github/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillianFuks/nvim-tree.lua/master/.github/screenshot2.png -------------------------------------------------------------------------------- /.github/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillianFuks/nvim-tree.lua/master/.github/screenshot3.png -------------------------------------------------------------------------------- /.github/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillianFuks/nvim-tree.lua/master/.github/screenshot4.png -------------------------------------------------------------------------------- /scripts/setup-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ln -sf ../../.hooks/pre-commit.sh .git/hooks/pre-commit 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | end_of_line = lf 6 | 7 | [*.lua] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "None" 7 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- vim: ft=lua tw=80 2 | 3 | -- Don't report unused self arguments of methods. 4 | self = false 5 | 6 | ignore = { 7 | "631", -- max_line_length 8 | } 9 | 10 | -- Global objects defined by the C code 11 | globals = { 12 | "vim", 13 | "TreeExplorer" 14 | } 15 | -------------------------------------------------------------------------------- /lua/nvim-tree/config.lua: -------------------------------------------------------------------------------- 1 | -- INFO: DEPRECATED FILE, DO NOT ADD ANYTHING IN THERE 2 | -- keeping to avoid breaking user configs. Will remove during a weekend. 3 | local M = {} 4 | 5 | -- TODO: remove this once the cb property is not supported in mappings 6 | function M.nvim_tree_callback(callback_name) 7 | return string.format("lua require'nvim-tree.actions.dispatch'.dispatch('%s')", callback_name) 8 | end 9 | 10 | return M 11 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", 3 | "runtime.version" : "Lua 5.1", 4 | "diagnostics": { 5 | "globals": [ 6 | "vim" 7 | ], 8 | "disable": [ 9 | "cast-local-type", 10 | "lowercase-global", 11 | "missing-parameter", 12 | "missing-return", 13 | "missing-return-value", 14 | "need-check-nil", 15 | "param-type-mismatch" 16 | ] 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/root/dir-up.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | local core = require "nvim-tree.core" 3 | 4 | local M = {} 5 | 6 | function M.fn(node) 7 | if not node or node.name == ".." then 8 | return require("nvim-tree.actions.root.change-dir").fn ".." 9 | else 10 | local newdir = vim.fn.fnamemodify(utils.path_remove_trailing(core.get_cwd()), ":h") 11 | require("nvim-tree.actions.root.change-dir").fn(newdir) 12 | return require("nvim-tree.actions.finders.find-file").fn(node.absolute_path) 13 | end 14 | end 15 | 16 | return M 17 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/node/run-command.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | local core = require "nvim-tree.core" 3 | 4 | local M = {} 5 | 6 | ---Retrieves the absolute path to the node. 7 | ---Safely handles the node representing the current directory 8 | ---(the topmost node in the nvim-tree window) 9 | local function get_node_path(node) 10 | if node.name == ".." then 11 | return utils.path_remove_trailing(core.get_cwd()) 12 | else 13 | return node.absolute_path 14 | end 15 | end 16 | 17 | function M.run_file_command(node) 18 | local node_path = get_node_path(node) 19 | vim.api.nvim_input(": " .. node_path .. "") 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | nvim-tree.lua is a file explorer / filesystem tree view plugin for neovim 2 | Copyright © 2019 Yazdani Kiyan 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/tree-modifiers/toggles.lua: -------------------------------------------------------------------------------- 1 | local view = require "nvim-tree.view" 2 | local filters = require "nvim-tree.explorer.filters" 3 | local renderer = require "nvim-tree.renderer" 4 | local reloaders = require "nvim-tree.actions.reloaders.reloaders" 5 | 6 | local M = {} 7 | 8 | function M.custom() 9 | filters.config.filter_custom = not filters.config.filter_custom 10 | return reloaders.reload_explorer() 11 | end 12 | 13 | function M.git_ignored() 14 | filters.config.filter_git_ignored = not filters.config.filter_git_ignored 15 | return reloaders.reload_explorer() 16 | end 17 | 18 | function M.dotfiles() 19 | filters.config.filter_dotfiles = not filters.config.filter_dotfiles 20 | return reloaders.reload_explorer() 21 | end 22 | 23 | function M.help() 24 | view.toggle_help() 25 | renderer.draw() 26 | end 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | luacheck: 13 | name: luacheck 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Prepare 19 | run: | 20 | sudo apt-get update 21 | sudo add-apt-repository universe 22 | sudo apt install luarocks -y 23 | sudo luarocks install luacheck 24 | - name: Run luacheck 25 | run: luacheck . 26 | stylua: 27 | name: stylua 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: JohnnyMorganz/stylua-action@1.0.0 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | args: --color always --check lua/ 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | **Is this a question?** 10 | Please start a new [Q&A discussion](https://github.com/kyazdani42/nvim-tree.lua/discussions/new) instead of raising a feature request. 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /lua/nvim-tree/core.lua: -------------------------------------------------------------------------------- 1 | local events = require "nvim-tree.events" 2 | local explorer = require "nvim-tree.explorer" 3 | local live_filter = require "nvim-tree.live-filter" 4 | local view = require "nvim-tree.view" 5 | 6 | local M = {} 7 | 8 | TreeExplorer = nil 9 | local first_init_done = false 10 | 11 | function M.init(foldername) 12 | if TreeExplorer then 13 | TreeExplorer:destroy() 14 | end 15 | TreeExplorer = explorer.Explorer.new(foldername) 16 | if not first_init_done then 17 | events._dispatch_ready() 18 | first_init_done = true 19 | end 20 | end 21 | 22 | function M.get_explorer() 23 | return TreeExplorer 24 | end 25 | 26 | function M.get_cwd() 27 | return TreeExplorer.absolute_path 28 | end 29 | 30 | function M.get_nodes_starting_line() 31 | local offset = 1 32 | if view.is_root_folder_visible(M.get_cwd()) then 33 | offset = offset + 1 34 | end 35 | if live_filter.filter then 36 | return offset + 1 37 | end 38 | return offset 39 | end 40 | 41 | return M 42 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/moves/parent.lua: -------------------------------------------------------------------------------- 1 | local renderer = require "nvim-tree.renderer" 2 | local view = require "nvim-tree.view" 3 | local utils = require "nvim-tree.utils" 4 | local core = require "nvim-tree.core" 5 | 6 | local M = {} 7 | 8 | function M.fn(should_close) 9 | should_close = should_close or false 10 | 11 | return function(node) 12 | if should_close and node.open then 13 | node.open = false 14 | return renderer.draw() 15 | end 16 | 17 | local parent = node.parent 18 | 19 | if renderer.config.group_empty and parent then 20 | while parent.parent and parent.parent.group_next do 21 | parent = parent.parent 22 | end 23 | end 24 | 25 | if not parent or not parent.parent then 26 | return view.set_cursor { 1, 0 } 27 | end 28 | 29 | local _, line = utils.find_node(core.get_explorer().nodes, function(n) 30 | return n.absolute_path == parent.absolute_path 31 | end) 32 | 33 | view.set_cursor { line + 1, 0 } 34 | if should_close then 35 | parent.open = false 36 | renderer.draw() 37 | end 38 | end 39 | end 40 | 41 | return M 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `nvim-tree.lua` 2 | 3 | Thank you for contributing. 4 | 5 | ## Styling and formatting 6 | 7 | Code is formatted using luacheck, and linted using stylua. 8 | You can install these with: 9 | 10 | ```bash 11 | luarocks install luacheck 12 | cargo install stylua 13 | ``` 14 | 15 | You can setup the git hooks by running `scripts/setup-hooks.sh`. 16 | 17 | ## Adding new actions 18 | 19 | To add a new action, add a file in `actions/name-of-the-action.lua`. You should export a `setup` function if some configuration is needed. 20 | Once you did, you should run the `scripts/update-help.sh`. 21 | 22 | ## Documentation 23 | 24 | When adding new options, you should declare the defaults in the main `nvim-tree.lua` file. 25 | Once you did, you should run the `scripts/update-help.sh`. 26 | 27 | Documentation for options should also be added, see how this is done after `nvim-tree.disable_netrw` in the `nvim-tree-lua.txt` file. 28 | 29 | ## Pull Request 30 | 31 | Please reference any issues in the description e.g. "resolves #1234". 32 | 33 | Please check "allow edits by maintainers" to allow nvim-tree developers to make small changes such as documentation tweaks. 34 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/tree-modifiers/collapse-all.lua: -------------------------------------------------------------------------------- 1 | local renderer = require "nvim-tree.renderer" 2 | local utils = require "nvim-tree.utils" 3 | local core = require "nvim-tree.core" 4 | local Iterator = require "nvim-tree.iterators.node-iterator" 5 | 6 | local M = {} 7 | 8 | local function buf_match() 9 | local buffer_paths = vim.tbl_map(function(buffer) 10 | return vim.api.nvim_buf_get_name(buffer) 11 | end, vim.api.nvim_list_bufs()) 12 | 13 | return function(path) 14 | for _, buffer_path in ipairs(buffer_paths) do 15 | local matches = utils.str_find(buffer_path, path) 16 | if matches then 17 | return true 18 | end 19 | end 20 | return false 21 | end 22 | end 23 | 24 | function M.fn(keep_buffers) 25 | if not core.get_explorer() then 26 | return 27 | end 28 | 29 | local matches = buf_match() 30 | 31 | Iterator.builder(core.get_explorer().nodes) 32 | :hidden() 33 | :applier(function(node) 34 | node.open = keep_buffers == true and matches(node.absolute_path) 35 | end) 36 | :recursor(function(n) 37 | return n.nodes 38 | end) 39 | :iterate() 40 | 41 | renderer.draw() 42 | end 43 | 44 | return M 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/nvt-min.lua: -------------------------------------------------------------------------------- 1 | vim.cmd [[set runtimepath=$VIMRUNTIME]] 2 | vim.cmd [[set packpath=/tmp/nvt-min/site]] 3 | local package_root = "/tmp/nvt-min/site/pack" 4 | local install_path = package_root .. "/packer/start/packer.nvim" 5 | local function load_plugins() 6 | require("packer").startup { 7 | { 8 | "wbthomason/packer.nvim", 9 | "kyazdani42/nvim-tree.lua", 10 | "kyazdani42/nvim-web-devicons", 11 | -- ADD PLUGINS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE 12 | }, 13 | config = { 14 | package_root = package_root, 15 | compile_path = install_path .. "/plugin/packer_compiled.lua", 16 | display = { non_interactive = true }, 17 | }, 18 | } 19 | end 20 | if vim.fn.isdirectory(install_path) == 0 then 21 | print "Installing nvim-tree and dependencies." 22 | vim.fn.system { "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path } 23 | end 24 | load_plugins() 25 | require("packer").sync() 26 | vim.cmd [[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]] 27 | vim.opt.termguicolors = true 28 | vim.opt.cursorline = true 29 | 30 | -- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE 31 | _G.setup = function() 32 | require("nvim-tree").setup {} 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lua/nvim-tree/marks/bulk-move.lua: -------------------------------------------------------------------------------- 1 | local Marks = require "nvim-tree.marks" 2 | local Core = require "nvim-tree.core" 3 | local utils = require "nvim-tree.utils" 4 | local FsRename = require "nvim-tree.actions.fs.rename-file" 5 | 6 | local M = {} 7 | 8 | function M.bulk_move() 9 | if #Marks.get_marks() == 0 then 10 | utils.notify.warn "no bookmark to perform bulk move on, aborting." 11 | return 12 | end 13 | 14 | vim.ui.input({ prompt = "Move to: ", default = Core.get_cwd(), completion = "dir" }, function(location) 15 | utils.clear_prompt() 16 | if not location or location == "" then 17 | return 18 | end 19 | if vim.fn.filewritable(location) ~= 2 then 20 | utils.notify.warn(location .. " is not writable, cannot move.") 21 | return 22 | end 23 | 24 | local marks = Marks.get_marks() 25 | for _, node in pairs(marks) do 26 | local head = vim.fn.fnamemodify(node.absolute_path, ":t") 27 | local to = utils.path_join { location, head } 28 | FsRename.rename(node, to) 29 | end 30 | 31 | if M.enable_reload then 32 | require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 33 | end 34 | end) 35 | end 36 | 37 | function M.setup(opts) 38 | M.enable_reload = not opts.filesystem_watchers.enable 39 | end 40 | 41 | return M 42 | -------------------------------------------------------------------------------- /scripts/update-help.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # run after changing nvim-tree.lua DEFAULT_OPTS or nvim-tree/actions/init.lua M.mappings 4 | # scrapes and updates nvim-tree-lua.txt 5 | # run from repository root: scripts/update-default-opts.sh 6 | 7 | 8 | begin="BEGIN_DEFAULT_OPTS" 9 | end="END_DEFAULT_OPTS" 10 | 11 | # scrape DEFAULT_OPTS, indented at 2 12 | sed -n -e "/${begin}/,/${end}/{ /${begin}/d; /${end}/d; p; }" lua/nvim-tree.lua > /tmp/DEFAULT_OPTS.2.lua 13 | 14 | # indent some more 15 | sed -e "s/^ / /" /tmp/DEFAULT_OPTS.2.lua > /tmp/DEFAULT_OPTS.6.lua 16 | 17 | # help, indented at 6 18 | sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_OPTS.6.lua 19 | }; /${end}/p; d; }" doc/nvim-tree-lua.txt 20 | 21 | 22 | begin="BEGIN_DEFAULT_MAPPINGS" 23 | end="END_DEFAULT_MAPPINGS" 24 | 25 | # generate various DEFAULT_MAPPINGS 26 | sed -n -e "/${begin}/,/${end}/{ /${begin}/d; /${end}/d; p; }" lua/nvim-tree/actions/init.lua > /tmp/DEFAULT_MAPPINGS.M.lua 27 | cat /tmp/DEFAULT_MAPPINGS.M.lua scripts/generate_default_mappings.lua | lua 28 | 29 | # help 30 | sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_MAPPINGS.lua 31 | }; /${end}/p; d }" doc/nvim-tree-lua.txt 32 | sed -i -e "/^DEFAULT MAPPINGS/,/^>$/{ /^DEFAULT MAPPINGS/{p; r /tmp/DEFAULT_MAPPINGS.help 33 | }; /^>$/p; d }" doc/nvim-tree-lua.txt 34 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/moves/sibling.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | local core = require "nvim-tree.core" 3 | local Iterator = require "nvim-tree.iterators.node-iterator" 4 | 5 | local M = {} 6 | 7 | function M.fn(direction) 8 | return function(node) 9 | if node.name == ".." or not direction then 10 | return 11 | end 12 | 13 | local first, last, next, prev = nil, nil, nil, nil 14 | local found = false 15 | local parent = node.parent or core.get_explorer() 16 | Iterator.builder(parent.nodes) 17 | :recursor(function() 18 | return nil 19 | end) 20 | :applier(function(n) 21 | first = first or n 22 | last = n 23 | if n.absolute_path == node.absolute_path then 24 | found = true 25 | return 26 | end 27 | prev = not found and n or prev 28 | if found and not next then 29 | next = n 30 | end 31 | end) 32 | :iterate() 33 | 34 | local target_node 35 | if direction == "first" then 36 | target_node = first 37 | elseif direction == "last" then 38 | target_node = last 39 | elseif direction == "next" then 40 | target_node = next or first 41 | else 42 | target_node = prev or last 43 | end 44 | 45 | if target_node then 46 | utils.focus_file(target_node.absolute_path) 47 | end 48 | end 49 | end 50 | 51 | return M 52 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/moves/item.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | local view = require "nvim-tree.view" 3 | local core = require "nvim-tree.core" 4 | local lib = require "nvim-tree.lib" 5 | 6 | local M = {} 7 | 8 | function M.fn(where, what) 9 | return function() 10 | local node_cur = lib.get_node_at_cursor() 11 | local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line()) 12 | 13 | local cur, first, prev, nex = nil, nil, nil, nil 14 | for line, node in pairs(nodes_by_line) do 15 | local valid = false 16 | if what == "git" then 17 | valid = node.git_status ~= nil 18 | elseif what == "diag" then 19 | valid = node.diag_status ~= nil 20 | end 21 | 22 | if not first and valid then 23 | first = line 24 | end 25 | 26 | if node == node_cur then 27 | cur = line 28 | elseif valid then 29 | if not cur then 30 | prev = line 31 | end 32 | if cur and not nex then 33 | nex = line 34 | break 35 | end 36 | end 37 | end 38 | 39 | if where == "prev" then 40 | if prev then 41 | view.set_cursor { prev, 0 } 42 | end 43 | else 44 | if cur then 45 | if nex then 46 | view.set_cursor { nex, 0 } 47 | end 48 | elseif first then 49 | view.set_cursor { first, 0 } 50 | end 51 | end 52 | end 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /lua/nvim-tree/iterators/node-iterator.lua: -------------------------------------------------------------------------------- 1 | local NodeIterator = {} 2 | NodeIterator.__index = NodeIterator 3 | 4 | function NodeIterator.builder(nodes) 5 | return setmetatable({ 6 | nodes = nodes, 7 | _filter_hidden = function(node) 8 | return not node.hidden 9 | end, 10 | _apply_fn_on_node = function(_) end, 11 | _match = function(_) end, 12 | _recurse_with = function(node) 13 | return node.nodes 14 | end, 15 | }, NodeIterator) 16 | end 17 | 18 | function NodeIterator:hidden() 19 | self._filter_hidden = function(_) 20 | return true 21 | end 22 | return self 23 | end 24 | 25 | function NodeIterator:matcher(f) 26 | self._match = f 27 | return self 28 | end 29 | 30 | function NodeIterator:applier(f) 31 | self._apply_fn_on_node = f 32 | return self 33 | end 34 | 35 | function NodeIterator:recursor(f) 36 | self._recurse_with = f 37 | return self 38 | end 39 | 40 | function NodeIterator:iterate() 41 | local iteration_count = 0 42 | local function iter(nodes) 43 | for _, node in ipairs(nodes) do 44 | if self._filter_hidden(node) then 45 | iteration_count = iteration_count + 1 46 | if self._match(node) then 47 | return node, iteration_count 48 | end 49 | self._apply_fn_on_node(node, iteration_count) 50 | local children = self._recurse_with(node) 51 | if children then 52 | local n = iter(children) 53 | if n then 54 | return n, iteration_count 55 | end 56 | end 57 | end 58 | end 59 | return nil, 0 60 | end 61 | 62 | return iter(self.nodes) 63 | end 64 | 65 | return NodeIterator 66 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/init.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local git = require "nvim-tree.git" 4 | local watch = require "nvim-tree.explorer.watch" 5 | local common = require "nvim-tree.explorer.common" 6 | 7 | local M = {} 8 | 9 | M.explore = require("nvim-tree.explorer.explore").explore 10 | M.reload = require("nvim-tree.explorer.reload").reload 11 | 12 | local Explorer = {} 13 | Explorer.__index = Explorer 14 | 15 | function Explorer.new(cwd) 16 | cwd = uv.fs_realpath(cwd or uv.cwd()) 17 | local explorer = setmetatable({ 18 | absolute_path = cwd, 19 | nodes = {}, 20 | watcher = watch.create_watcher(cwd), 21 | open = true, 22 | }, Explorer) 23 | explorer:_load(explorer) 24 | return explorer 25 | end 26 | 27 | function Explorer:_load(node) 28 | local cwd = node.link_to or node.absolute_path 29 | local git_statuses = git.load_project_status(cwd) 30 | M.explore(node, git_statuses) 31 | end 32 | 33 | function Explorer:expand(node) 34 | self:_load(node) 35 | end 36 | 37 | function Explorer:destroy() 38 | local function iterate(node) 39 | common.node_destroy(node) 40 | if node.nodes then 41 | for _, child in pairs(node.nodes) do 42 | iterate(child) 43 | end 44 | end 45 | end 46 | iterate(self) 47 | end 48 | 49 | function M.setup(opts) 50 | require("nvim-tree.explorer.common").setup(opts) 51 | require("nvim-tree.explorer.explore").setup(opts) 52 | require("nvim-tree.explorer.filters").setup(opts) 53 | require("nvim-tree.explorer.sorters").setup(opts) 54 | require("nvim-tree.explorer.reload").setup(opts) 55 | require("nvim-tree.explorer.watch").setup(opts) 56 | end 57 | 58 | M.Explorer = Explorer 59 | 60 | return M 61 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/common.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local M = {} 4 | 5 | local function get_dir_git_status(parent_ignored, status, absolute_path) 6 | if parent_ignored then 7 | return "!!" 8 | end 9 | 10 | local file_status = status.files and status.files[absolute_path] 11 | if file_status then 12 | return file_status 13 | end 14 | 15 | if M.config.git.show_on_dirs then 16 | return status.dirs and status.dirs[absolute_path] 17 | end 18 | end 19 | 20 | local function get_git_status(parent_ignored, status, absolute_path) 21 | return parent_ignored and "!!" or status.files and status.files[absolute_path] 22 | end 23 | 24 | function M.has_one_child_folder(node) 25 | return #node.nodes == 1 and node.nodes[1].nodes and uv.fs_access(node.nodes[1].absolute_path, "R") 26 | end 27 | 28 | function M.update_git_status(node, parent_ignored, status) 29 | -- status of the node's absolute path 30 | if node.nodes then 31 | node.git_status = get_dir_git_status(parent_ignored, status, node.absolute_path) 32 | else 33 | node.git_status = get_git_status(parent_ignored, status, node.absolute_path) 34 | end 35 | 36 | -- status of the link target, if the link itself is not dirty 37 | if node.link_to and not node.git_status then 38 | if node.nodes then 39 | node.git_status = get_dir_git_status(parent_ignored, status, node.link_to) 40 | else 41 | node.git_status = get_git_status(parent_ignored, status, node.link_to) 42 | end 43 | end 44 | end 45 | 46 | function M.node_destroy(node) 47 | if not node then 48 | return 49 | end 50 | 51 | if node.watcher then 52 | node.watcher:destroy() 53 | end 54 | end 55 | 56 | function M.setup(opts) 57 | M.config = { 58 | git = opts.git, 59 | } 60 | end 61 | 62 | return M 63 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/fs/rename-file.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local lib = require "nvim-tree.lib" 4 | local utils = require "nvim-tree.utils" 5 | local events = require "nvim-tree.events" 6 | 7 | local M = {} 8 | 9 | local function err_fmt(from, to, reason) 10 | return string.format("Cannot rename %s -> %s: %s", from, to, reason) 11 | end 12 | 13 | function M.rename(node, to) 14 | if utils.file_exists(to) then 15 | utils.notify.warn(err_fmt(node.absolute_path, to, "file already exists")) 16 | return 17 | end 18 | 19 | local success, err = uv.fs_rename(node.absolute_path, to) 20 | if not success then 21 | return utils.notify.warn(err_fmt(node.absolute_path, to, err)) 22 | end 23 | utils.notify.info(node.absolute_path .. " ➜ " .. to) 24 | utils.rename_loaded_buffers(node.absolute_path, to) 25 | events._dispatch_node_renamed(node.absolute_path, to) 26 | end 27 | 28 | function M.fn(with_sub) 29 | return function(node) 30 | node = lib.get_last_group_node(node) 31 | if node.name == ".." then 32 | return 33 | end 34 | 35 | local namelen = node.name:len() 36 | local abs_path = with_sub and node.absolute_path:sub(0, namelen * -1 - 1) or node.absolute_path 37 | 38 | local input_opts = { prompt = "Rename to ", default = abs_path, completion = "file" } 39 | 40 | vim.ui.input(input_opts, function(new_file_path) 41 | utils.clear_prompt() 42 | if not new_file_path then 43 | return 44 | end 45 | 46 | M.rename(node, new_file_path) 47 | if M.enable_reload then 48 | require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 49 | end 50 | end) 51 | end 52 | end 53 | 54 | function M.setup(opts) 55 | M.enable_reload = not opts.filesystem_watchers.enable 56 | end 57 | 58 | return M 59 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/help.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.compute_lines() 4 | local help_lines = { "HELP" } 5 | local help_hl = { { "NvimTreeRootFolder", 0, 0, #help_lines[1] } } 6 | local mappings = vim.tbl_filter(function(v) 7 | return (v.cb ~= nil and v.cb ~= "") or (v.action ~= nil and v.action ~= "") 8 | end, require("nvim-tree.actions").mappings) 9 | local processed = {} 10 | for _, b in pairs(mappings) do 11 | local cb = b.cb 12 | local key = b.key 13 | local name 14 | if cb and cb:sub(1, 35) == require("nvim-tree.config").nvim_tree_callback("test"):sub(1, 35) then 15 | name = cb:match "'[^']+'[^']*$" 16 | name = name:match "'[^']+'" 17 | elseif b.action then 18 | name = b.action 19 | else 20 | name = (b.name ~= nil) and b.name or cb 21 | name = '"' .. name .. '"' 22 | end 23 | table.insert(processed, { key, name, true }) 24 | end 25 | table.sort(processed, function(a, b) 26 | return (a[3] == b[3] and (a[2] < b[2] or (a[2] == b[2] and #a[1] < #b[1]))) or (a[3] and not b[3]) 27 | end) 28 | 29 | local num = 0 30 | for _, val in pairs(processed) do 31 | local keys = type(val[1]) == "string" and { val[1] } or val[1] 32 | local map_name = val[2] 33 | local builtin = val[3] 34 | for _, key in pairs(keys) do 35 | num = num + 1 36 | local bind_string = string.format("%6s : %s", key, map_name) 37 | table.insert(help_lines, bind_string) 38 | 39 | local hl_len = math.max(6, string.len(key)) + 2 40 | table.insert(help_hl, { "NvimTreeFolderName", num, 0, hl_len }) 41 | 42 | if not builtin then 43 | table.insert(help_hl, { "NvimTreeFileRenamed", num, hl_len, -1 }) 44 | end 45 | end 46 | end 47 | return help_lines, help_hl 48 | end 49 | 50 | return M 51 | -------------------------------------------------------------------------------- /lua/nvim-tree/marks/init.lua: -------------------------------------------------------------------------------- 1 | local view = require "nvim-tree.view" 2 | local Iterator = require "nvim-tree.iterators.node-iterator" 3 | local core = require "nvim-tree.core" 4 | 5 | local NvimTreeMarks = {} 6 | 7 | local M = {} 8 | 9 | local function add_mark(node) 10 | NvimTreeMarks[node.absolute_path] = node 11 | M.draw() 12 | end 13 | 14 | local function remove_mark(node) 15 | NvimTreeMarks[node.absolute_path] = nil 16 | M.draw() 17 | end 18 | 19 | function M.toggle_mark(node) 20 | if node.absolute_path == nil then 21 | return 22 | end 23 | 24 | if M.get_mark(node) then 25 | remove_mark(node) 26 | else 27 | add_mark(node) 28 | end 29 | end 30 | 31 | function M.get_mark(node) 32 | return NvimTreeMarks[node.absolute_path] 33 | end 34 | 35 | function M.get_marks() 36 | local list = {} 37 | for _, node in pairs(NvimTreeMarks) do 38 | table.insert(list, node) 39 | end 40 | return list 41 | end 42 | 43 | local GROUP = "NvimTreeMarkSigns" 44 | local SIGN_NAME = "NvimTreeMark" 45 | 46 | function M.clear() 47 | vim.fn.sign_unplace(GROUP) 48 | end 49 | 50 | function M.draw() 51 | if not view.is_visible() then 52 | return 53 | end 54 | 55 | M.clear() 56 | 57 | local buf = view.get_bufnr() 58 | local add = core.get_nodes_starting_line() - 1 59 | Iterator.builder(core.get_explorer().nodes) 60 | :recursor(function(node) 61 | return node.open and node.nodes 62 | end) 63 | :applier(function(node, idx) 64 | if M.get_mark(node) then 65 | vim.fn.sign_place(0, GROUP, SIGN_NAME, buf, { lnum = idx + add, priority = 3 }) 66 | end 67 | end) 68 | :iterate() 69 | end 70 | 71 | function M.setup(opts) 72 | vim.fn.sign_define(SIGN_NAME, { text = opts.renderer.icons.glyphs.bookmark, texthl = "NvimTreeBookmark" }) 73 | require("nvim-tree.marks.bulk-move").setup(opts) 74 | end 75 | 76 | return M 77 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/reloaders/reloaders.lua: -------------------------------------------------------------------------------- 1 | local git = require "nvim-tree.git" 2 | local view = require "nvim-tree.view" 3 | local renderer = require "nvim-tree.renderer" 4 | local explorer_module = require "nvim-tree.explorer" 5 | local core = require "nvim-tree.core" 6 | 7 | local M = {} 8 | 9 | local function refresh_nodes(node, projects) 10 | local cwd = node.cwd or node.link_to or node.absolute_path 11 | local project_root = git.get_project_root(cwd) 12 | explorer_module.reload(node, projects[project_root] or {}) 13 | for _, _node in ipairs(node.nodes) do 14 | if _node.nodes and _node.open then 15 | refresh_nodes(_node, projects) 16 | end 17 | end 18 | end 19 | 20 | function M.reload_node_status(parent_node, projects) 21 | local project_root = git.get_project_root(parent_node.absolute_path) 22 | local status = projects[project_root] or {} 23 | for _, node in ipairs(parent_node.nodes) do 24 | if node.nodes then 25 | node.git_status = status.dirs and status.dirs[node.absolute_path] 26 | else 27 | node.git_status = status.files and status.files[node.absolute_path] 28 | end 29 | if node.nodes and #node.nodes > 0 then 30 | M.reload_node_status(node, projects) 31 | end 32 | end 33 | end 34 | 35 | local event_running = false 36 | function M.reload_explorer() 37 | if event_running or not core.get_explorer() or vim.v.exiting ~= vim.NIL then 38 | return 39 | end 40 | event_running = true 41 | 42 | local projects = git.reload() 43 | refresh_nodes(core.get_explorer(), projects) 44 | if view.is_visible() then 45 | renderer.draw() 46 | end 47 | event_running = false 48 | end 49 | 50 | function M.reload_git() 51 | if not core.get_explorer() or not git.config.enable or event_running then 52 | return 53 | end 54 | event_running = true 55 | 56 | local projects = git.reload() 57 | M.reload_node_status(core.get_explorer(), projects) 58 | renderer.draw() 59 | event_running = false 60 | end 61 | 62 | return M 63 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/filters.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | 3 | local M = { 4 | ignore_list = {}, 5 | exclude_list = {}, 6 | } 7 | 8 | local function is_excluded(path) 9 | for _, node in ipairs(M.exclude_list) do 10 | if path:match(node) then 11 | return true 12 | end 13 | end 14 | return false 15 | end 16 | 17 | ---Check if the given path should be ignored. 18 | ---@param path string Absolute path 19 | ---@return boolean 20 | function M.should_ignore(path) 21 | local basename = utils.path_basename(path) 22 | 23 | if is_excluded(path) then 24 | return false 25 | end 26 | 27 | if M.config.filter_dotfiles then 28 | if basename:sub(1, 1) == "." then 29 | return true 30 | end 31 | end 32 | 33 | if not M.config.filter_custom then 34 | return false 35 | end 36 | 37 | local relpath = utils.path_relative(path, vim.loop.cwd()) 38 | for pat, _ in pairs(M.ignore_list) do 39 | if vim.fn.match(relpath, pat) ~= -1 or vim.fn.match(basename, pat) ~= -1 then 40 | return true 41 | end 42 | end 43 | 44 | local idx = path:match ".+()%.[^.]+$" 45 | if idx then 46 | if M.ignore_list["*" .. string.sub(path, idx)] == true then 47 | return true 48 | end 49 | end 50 | 51 | return false 52 | end 53 | 54 | function M.should_ignore_git(path, status) 55 | return M.config.filter_git_ignored 56 | and (M.config.filter_git_ignored and status and status[path] == "!!") 57 | and not is_excluded(path) 58 | end 59 | 60 | function M.setup(opts) 61 | M.config = { 62 | filter_custom = true, 63 | filter_dotfiles = opts.filters.dotfiles, 64 | filter_git_ignored = opts.git.ignore, 65 | } 66 | 67 | M.ignore_list = {} 68 | M.exclude_list = opts.filters.exclude 69 | 70 | local custom_filter = opts.filters.custom 71 | if custom_filter and #custom_filter > 0 then 72 | for _, filter_name in pairs(custom_filter) do 73 | M.ignore_list[filter_name] = true 74 | end 75 | end 76 | end 77 | 78 | return M 79 | -------------------------------------------------------------------------------- /lua/nvim-tree/log.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local M = { 4 | config = nil, 5 | path = nil, 6 | } 7 | 8 | --- Write to log file 9 | --- @param typ string as per log.types config 10 | --- @param fmt string for string.format 11 | --- @vararg any arguments for string.format 12 | function M.raw(typ, fmt, ...) 13 | if not M.path or not M.config.types[typ] and not M.config.types.all then 14 | return 15 | end 16 | 17 | local line = string.format(fmt, ...) 18 | local file = io.open(M.path, "a") 19 | io.output(file) 20 | io.write(line) 21 | io.close(file) 22 | end 23 | 24 | --- Write to log file via M.line 25 | --- START is prefixed 26 | --- @return number nanos to pass to profile_end 27 | function M.profile_start(fmt, ...) 28 | if not M.path or not M.config.types.profile and not M.config.types.all then 29 | return 30 | end 31 | M.line("profile", "START " .. (fmt or "???"), ...) 32 | return uv.hrtime() 33 | end 34 | 35 | --- Write to log file via M.line 36 | --- END is prefixed and duration in seconds is suffixed 37 | --- @param start number nanos returned from profile_start 38 | function M.profile_end(start, fmt, ...) 39 | if not M.path or not M.config.types.profile and not M.config.types.all then 40 | return 41 | end 42 | local millis = start and math.modf((uv.hrtime() - start) / 1000000) or -1 43 | M.line("profile", "END " .. (fmt or "???") .. " " .. millis .. "ms", ...) 44 | end 45 | 46 | -- Write to log file via M.raw 47 | -- time and typ are prefixed and a trailing newline is added 48 | function M.line(typ, fmt, ...) 49 | M.raw(typ, string.format("[%s] [%s] %s\n", os.date "%Y-%m-%d %H:%M:%S", typ, fmt), ...) 50 | end 51 | 52 | function M.setup(opts) 53 | M.config = opts.log 54 | if M.config and M.config.enable and M.config.types then 55 | M.path = string.format("%s/nvim-tree.log", vim.fn.stdpath "cache", os.date "%H:%M:%S", vim.env.USER) 56 | if M.config.truncate then 57 | os.remove(M.path) 58 | end 59 | require("nvim-tree.utils").notify.debug("nvim-tree.lua logging to " .. M.path) 60 | end 61 | end 62 | 63 | return M 64 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/finders/find-file.lua: -------------------------------------------------------------------------------- 1 | local log = require "nvim-tree.log" 2 | local uv = vim.loop 3 | local view = require "nvim-tree.view" 4 | local utils = require "nvim-tree.utils" 5 | local renderer = require "nvim-tree.renderer" 6 | local core = require "nvim-tree.core" 7 | local Iterator = require "nvim-tree.iterators.node-iterator" 8 | 9 | local M = {} 10 | 11 | local running = {} 12 | 13 | ---Find a path in the tree, expand it and focus it 14 | ---@param fname string full path 15 | function M.fn(fname) 16 | if running[fname] or not core.get_explorer() then 17 | return 18 | end 19 | running[fname] = true 20 | 21 | local ps = log.profile_start("find file %s", fname) 22 | -- always match against the real path 23 | local fname_real = uv.fs_realpath(fname) 24 | if not fname_real then 25 | return 26 | end 27 | 28 | local line = core.get_nodes_starting_line() 29 | 30 | local absolute_paths_searched = {} 31 | 32 | local found = Iterator.builder(core.get_explorer().nodes) 33 | :matcher(function(node) 34 | return node.absolute_path == fname_real or node.link_to == fname_real 35 | end) 36 | :applier(function(node) 37 | line = line + 1 38 | 39 | if vim.tbl_contains(absolute_paths_searched, node.absolute_path) then 40 | return 41 | end 42 | table.insert(absolute_paths_searched, node.absolute_path) 43 | 44 | local abs_match = vim.startswith(fname_real, node.absolute_path .. utils.path_separator) 45 | local link_match = node.link_to and vim.startswith(fname_real, node.link_to .. utils.path_separator) 46 | 47 | if abs_match or link_match then 48 | node.open = true 49 | if #node.nodes == 0 then 50 | core.get_explorer():expand(node) 51 | end 52 | end 53 | end) 54 | :recursor(function(node) 55 | return node.open and node.nodes 56 | end) 57 | :iterate() 58 | 59 | if found and view.is_visible() then 60 | renderer.draw() 61 | view.set_cursor { line, 0 } 62 | end 63 | 64 | running[fname] = false 65 | 66 | log.profile_end(ps, "find file %s", fname) 67 | end 68 | 69 | return M 70 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/tree-modifiers/expand-all.lua: -------------------------------------------------------------------------------- 1 | local core = require "nvim-tree.core" 2 | local renderer = require "nvim-tree.renderer" 3 | local utils = require "nvim-tree.utils" 4 | local Iterator = require "nvim-tree.iterators.node-iterator" 5 | 6 | local M = {} 7 | 8 | local function to_lookup_table(list) 9 | local table = {} 10 | for _, element in ipairs(list) do 11 | table[element] = true 12 | end 13 | 14 | return table 15 | end 16 | 17 | local function expand(node) 18 | node.open = true 19 | if #node.nodes == 0 then 20 | core.get_explorer():expand(node) 21 | end 22 | end 23 | 24 | local function should_expand(expansion_count, node) 25 | local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY 26 | local should_exclude = M.EXCLUDE[node.name] 27 | return not should_halt and node.nodes and not node.open and not should_exclude 28 | end 29 | 30 | local function gen_iterator() 31 | local expansion_count = 0 32 | 33 | return function(parent) 34 | if parent.parent and parent.nodes and not parent.open then 35 | expansion_count = expansion_count + 1 36 | expand(parent) 37 | end 38 | 39 | Iterator.builder(parent.nodes) 40 | :hidden() 41 | :applier(function(node) 42 | if should_expand(expansion_count, node) then 43 | expansion_count = expansion_count + 1 44 | expand(node) 45 | end 46 | end) 47 | :recursor(function(node) 48 | return expansion_count < M.MAX_FOLDER_DISCOVERY and node.open and node.nodes 49 | end) 50 | :iterate() 51 | 52 | if expansion_count >= M.MAX_FOLDER_DISCOVERY then 53 | return true 54 | end 55 | end 56 | end 57 | 58 | function M.fn(base_node) 59 | local node = base_node.nodes and base_node or core.get_explorer() 60 | if gen_iterator()(node) then 61 | utils.notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders") 62 | end 63 | renderer.draw() 64 | end 65 | 66 | function M.setup(opts) 67 | M.MAX_FOLDER_DISCOVERY = opts.actions.expand_all.max_folder_discovery 68 | M.EXCLUDE = to_lookup_table(opts.actions.expand_all.exclude) 69 | end 70 | 71 | return M 72 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/node/system-open.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local M = { 4 | config = { 5 | is_windows = vim.fn.has "win32" == 1 or vim.fn.has "win32unix" == 1, 6 | is_macos = vim.fn.has "mac" == 1 or vim.fn.has "macunix" == 1, 7 | is_unix = vim.fn.has "unix" == 1, 8 | }, 9 | } 10 | 11 | function M.fn(node) 12 | if #M.config.system_open.cmd == 0 then 13 | require("nvim-tree.utils").notify.warn "Cannot open file with system application. Unrecognized platform." 14 | return 15 | end 16 | 17 | local process = { 18 | cmd = M.config.system_open.cmd, 19 | args = M.config.system_open.args, 20 | errors = "\n", 21 | stderr = uv.new_pipe(false), 22 | } 23 | table.insert(process.args, node.link_to or node.absolute_path) 24 | process.handle, process.pid = uv.spawn( 25 | process.cmd, 26 | { args = process.args, stdio = { nil, nil, process.stderr }, detached = true }, 27 | function(code) 28 | process.stderr:read_stop() 29 | process.stderr:close() 30 | process.handle:close() 31 | if code ~= 0 then 32 | process.errors = process.errors .. string.format("NvimTree system_open: return code %d.", code) 33 | error(process.errors) 34 | end 35 | end 36 | ) 37 | table.remove(process.args) 38 | if not process.handle then 39 | error("\n" .. process.pid .. "\nNvimTree system_open: failed to spawn process using '" .. process.cmd .. "'.") 40 | return 41 | end 42 | uv.read_start(process.stderr, function(err, data) 43 | if err then 44 | return 45 | end 46 | if data then 47 | process.errors = process.errors .. data 48 | end 49 | end) 50 | uv.unref(process.handle) 51 | end 52 | 53 | function M.setup(opts) 54 | M.config.system_open = opts.system_open or {} 55 | 56 | if #M.config.system_open.cmd == 0 then 57 | if M.config.is_windows then 58 | M.config.system_open = { 59 | cmd = "cmd", 60 | args = { "/c", "start", '""' }, 61 | } 62 | elseif M.config.is_macos then 63 | M.config.system_open.cmd = "open" 64 | elseif M.config.is_unix then 65 | M.config.system_open.cmd = "xdg-open" 66 | end 67 | end 68 | end 69 | 70 | return M 71 | -------------------------------------------------------------------------------- /lua/nvim-tree/git/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local log = require "nvim-tree.log" 3 | 4 | local has_cygpath = vim.fn.executable "cygpath" == 1 5 | 6 | function M.get_toplevel(cwd) 7 | 8 | local ps = log.profile_start("git toplevel %s", cwd) 9 | 10 | local cmd = { "git", "-C", cwd, "rev-parse", "--show-toplevel" } 11 | log.line("git", "%s", vim.inspect(cmd)) 12 | 13 | local toplevel = vim.fn.system(cmd) 14 | 15 | log.raw("git", toplevel) 16 | log.profile_end(ps, "git toplevel %s", cwd) 17 | 18 | if vim.v.shell_error ~= 0 or not toplevel or #toplevel == 0 or toplevel:match "fatal" then 19 | return nil 20 | end 21 | 22 | -- git always returns path with forward slashes 23 | if vim.fn.has "win32" == 1 then 24 | -- msys2 git support 25 | if has_cygpath then 26 | toplevel = vim.fn.system("cygpath -w " .. vim.fn.shellescape(toplevel)) 27 | if vim.v.shell_error ~= 0 then 28 | return nil 29 | end 30 | end 31 | toplevel = toplevel:gsub("/", "\\") 32 | end 33 | 34 | -- remove newline 35 | return toplevel:sub(0, -2) 36 | end 37 | 38 | local untracked = {} 39 | 40 | function M.should_show_untracked(cwd) 41 | if untracked[cwd] ~= nil then 42 | return untracked[cwd] 43 | end 44 | 45 | local ps = log.profile_start("git untracked %s", cwd) 46 | 47 | local cmd = { "git", "-C", cwd, "config", "status.showUntrackedFiles" } 48 | log.line("git", vim.inspect(cmd)) 49 | 50 | local has_untracked = vim.fn.system(cmd) 51 | 52 | log.raw("git", has_untracked) 53 | log.profile_end(ps, "git untracked %s", cwd) 54 | 55 | untracked[cwd] = vim.trim(has_untracked) ~= "no" 56 | return untracked[cwd] 57 | end 58 | 59 | function M.file_status_to_dir_status(status, cwd) 60 | local dirs = {} 61 | for p, s in pairs(status) do 62 | if s ~= "!!" then 63 | local modified = vim.fn.fnamemodify(p, ":h") 64 | dirs[modified] = s 65 | end 66 | end 67 | 68 | for dirname, s in pairs(dirs) do 69 | local modified = dirname 70 | while modified ~= cwd and modified ~= "/" do 71 | modified = vim.fn.fnamemodify(modified, ":h") 72 | dirs[modified] = s 73 | end 74 | end 75 | 76 | return dirs 77 | end 78 | 79 | return M 80 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/node/file-popup.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | local a = vim.api 3 | 4 | local M = {} 5 | 6 | local function get_formatted_lines(node) 7 | local stats = node.fs_stat 8 | local fpath = " fullpath: " .. node.absolute_path 9 | local created_at = " created: " .. os.date("%x %X", stats.birthtime.sec) 10 | local modified_at = " modified: " .. os.date("%x %X", stats.mtime.sec) 11 | local accessed_at = " accessed: " .. os.date("%x %X", stats.atime.sec) 12 | local size = " size: " .. utils.format_bytes(stats.size) 13 | 14 | return { 15 | fpath, 16 | size, 17 | accessed_at, 18 | modified_at, 19 | created_at, 20 | } 21 | end 22 | 23 | local current_popup = nil 24 | 25 | local function setup_window(node) 26 | local lines = get_formatted_lines(node) 27 | 28 | local max_width = vim.fn.max(vim.tbl_map(function(n) 29 | return #n 30 | end, lines)) 31 | local open_win_config = vim.tbl_extend("force", M.open_win_config, { 32 | width = max_width + 1, 33 | height = #lines, 34 | noautocmd = true, 35 | zindex = 60, 36 | }) 37 | local winnr = a.nvim_open_win(0, false, open_win_config) 38 | current_popup = { 39 | winnr = winnr, 40 | file_path = node.absolute_path, 41 | } 42 | local bufnr = a.nvim_create_buf(false, true) 43 | a.nvim_buf_set_lines(bufnr, 0, -1, false, lines) 44 | a.nvim_win_set_buf(winnr, bufnr) 45 | end 46 | 47 | function M.close_popup() 48 | if current_popup ~= nil then 49 | a.nvim_win_close(current_popup.winnr, { force = true }) 50 | vim.cmd "augroup NvimTreeRemoveFilePopup | au! CursorMoved | augroup END" 51 | 52 | current_popup = nil 53 | end 54 | end 55 | 56 | function M.toggle_file_info(node) 57 | if node.name == ".." then 58 | return 59 | end 60 | if current_popup ~= nil then 61 | local is_same_node = current_popup.file_path == node.absolute_path 62 | 63 | M.close_popup() 64 | 65 | if is_same_node then 66 | return 67 | end 68 | end 69 | 70 | setup_window(node) 71 | 72 | a.nvim_create_autocmd("CursorMoved", { 73 | group = a.nvim_create_augroup("NvimTreeRemoveFilePopup", {}), 74 | callback = M.close_popup, 75 | }) 76 | end 77 | 78 | function M.setup(opts) 79 | M.open_win_config = opts.actions.file_popup.open_win_config 80 | end 81 | 82 | return M 83 | -------------------------------------------------------------------------------- /lua/nvim-tree/marks/navigation.lua: -------------------------------------------------------------------------------- 1 | local Iterator = require "nvim-tree.iterators.node-iterator" 2 | local core = require "nvim-tree.core" 3 | local Marks = require "nvim-tree.marks" 4 | local open_file = require "nvim-tree.actions.node.open-file" 5 | local utils = require "nvim-tree.utils" 6 | local lib = require "nvim-tree.lib" 7 | 8 | local function get_nearest(node, where) 9 | local first, prev, next, last = nil, nil, nil, nil 10 | local found = false 11 | 12 | Iterator.builder(core.get_explorer().nodes) 13 | :recursor(function(n) 14 | return n.open and n.nodes 15 | end) 16 | :applier(function(n) 17 | if n.absolute_path == node.absolute_path then 18 | found = true 19 | return 20 | end 21 | 22 | if not Marks.get_mark(n) then 23 | return 24 | end 25 | 26 | last = n 27 | first = first or n 28 | 29 | if found and not next then 30 | next = n 31 | end 32 | 33 | if not found then 34 | prev = n 35 | end 36 | end) 37 | :iterate() 38 | 39 | if not found then 40 | return 41 | end 42 | 43 | if where == "next" then 44 | return next or first 45 | else 46 | return prev or last 47 | end 48 | end 49 | 50 | local function get(where, node) 51 | if node then 52 | return get_nearest(node, where) 53 | end 54 | end 55 | 56 | local function open_or_focus(node) 57 | if node and not node.nodes and not utils.get_win_buf_from_path(node.absolute_path) then 58 | open_file.fn("edit", node.absolute_path) 59 | elseif node then 60 | utils.focus_file(node.absolute_path) 61 | end 62 | end 63 | 64 | local function navigate_to(where) 65 | return function() 66 | local node = lib.get_node_at_cursor() 67 | local next = get(where, node) 68 | open_or_focus(next) 69 | end 70 | end 71 | 72 | local M = {} 73 | 74 | M.next = navigate_to "next" 75 | M.prev = navigate_to "prev" 76 | 77 | function M.select() 78 | local list = vim.tbl_map(function(n) 79 | return n.absolute_path 80 | end, Marks.get_marks()) 81 | 82 | vim.ui.select(list, { 83 | prompt = "Select a file to open or a folder to focus", 84 | }, function(choice) 85 | if not choice or choice == "" then 86 | return 87 | end 88 | local node = Marks.get_mark { absolute_path = choice } 89 | open_or_focus(node) 90 | end) 91 | end 92 | 93 | return M 94 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/finders/search-node.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local uv = vim.loop 3 | 4 | local core = require "nvim-tree.core" 5 | local filters = require "nvim-tree.explorer.filters" 6 | local find_file = require("nvim-tree.actions.finders.find-file").fn 7 | 8 | local M = {} 9 | 10 | local function search(search_dir, input_path) 11 | local realpaths_searched = {} 12 | 13 | if not search_dir then 14 | return 15 | end 16 | 17 | local function iter(dir) 18 | local realpath, path, name, stat, handle, _ 19 | 20 | handle, _ = uv.fs_scandir(dir) 21 | if not handle then 22 | return 23 | end 24 | 25 | realpath, _ = uv.fs_realpath(dir) 26 | if not realpath or vim.tbl_contains(realpaths_searched, realpath) then 27 | return 28 | end 29 | table.insert(realpaths_searched, realpath) 30 | 31 | name, _ = uv.fs_scandir_next(handle) 32 | while name do 33 | path = dir .. "/" .. name 34 | 35 | stat, _ = uv.fs_stat(path) 36 | if not stat then 37 | break 38 | end 39 | 40 | if not filters.should_ignore(path) then 41 | if string.find(path, "/" .. input_path .. "$") then 42 | return path 43 | end 44 | 45 | if stat.type == "directory" then 46 | path = iter(path) 47 | if path then 48 | return path 49 | end 50 | end 51 | end 52 | 53 | name, _ = uv.fs_scandir_next(handle) 54 | end 55 | end 56 | 57 | return iter(search_dir) 58 | end 59 | 60 | function M.fn() 61 | if not core.get_explorer() then 62 | return 63 | end 64 | 65 | -- temporarily set &path 66 | local bufnr = api.nvim_get_current_buf() 67 | local path_existed, path_opt = pcall(api.nvim_buf_get_option, bufnr, "path") 68 | api.nvim_buf_set_option(bufnr, "path", core.get_cwd() .. "/**") 69 | 70 | vim.ui.input({ prompt = "Search: ", completion = "file_in_path" }, function(input_path) 71 | if not input_path or input_path == "" then 72 | return 73 | end 74 | -- reset &path 75 | if path_existed then 76 | api.nvim_buf_set_option(bufnr, "path", path_opt) 77 | else 78 | api.nvim_buf_set_option(bufnr, "path", nil) 79 | end 80 | 81 | -- strip trailing slash 82 | input_path = string.gsub(input_path, "/$", "") 83 | 84 | -- search under cwd 85 | local found = search(core.get_cwd(), input_path) 86 | if found then 87 | find_file(found) 88 | end 89 | end) 90 | end 91 | 92 | return M 93 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/components/full-name.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local api = vim.api 4 | local fn = vim.fn 5 | 6 | local function hide(win) 7 | if win then 8 | if api.nvim_win_is_valid(win) then 9 | api.nvim_win_close(win, true) 10 | end 11 | end 12 | end 13 | 14 | local function show() 15 | local line_nr = api.nvim_win_get_cursor(0)[1] 16 | if line_nr == 1 and require("nvim-tree.view").is_root_folder_visible() then 17 | return 18 | end 19 | if vim.wo.wrap then 20 | return 21 | end 22 | -- only work for left tree 23 | if vim.api.nvim_win_get_position(0)[2] ~= 0 then 24 | return 25 | end 26 | 27 | local line = fn.getline "." 28 | local leftcol = fn.winsaveview().leftcol 29 | -- hide full name if left column of node in nvim-tree win is not zero 30 | if leftcol ~= 0 then 31 | return 32 | end 33 | 34 | local width = fn.strdisplaywidth(fn.substitute(line, "[^[:print:]]*$", "", "g")) 35 | if width < fn.winwidth(0) then 36 | return 37 | end 38 | M.popup_win = api.nvim_open_win(api.nvim_create_buf(false, false), false, { 39 | relative = "win", 40 | bufpos = { fn.line "." - 2, 0 }, 41 | width = math.min(width, vim.o.columns - 2), 42 | height = 1, 43 | noautocmd = true, 44 | style = "minimal", 45 | }) 46 | 47 | local ns_id = api.nvim_get_namespaces()["NvimTreeHighlights"] 48 | local extmarks = api.nvim_buf_get_extmarks(0, ns_id, { line_nr - 1, 0 }, { line_nr - 1, -1 }, { details = 1 }) 49 | api.nvim_win_call(M.popup_win, function() 50 | fn.setbufline("%", 1, line) 51 | for _, extmark in ipairs(extmarks) do 52 | local hl = extmark[4] 53 | api.nvim_buf_add_highlight(0, ns_id, hl.hl_group, 0, extmark[3], hl.end_col) 54 | end 55 | vim.cmd [[ setlocal nowrap cursorline noswapfile nobuflisted buftype=nofile bufhidden=hide ]] 56 | end) 57 | end 58 | 59 | M.setup = function(opts) 60 | M.config = opts.renderer 61 | if not M.config.full_name then 62 | return 63 | end 64 | 65 | local group = api.nvim_create_augroup("nvim_tree_floating_node", { clear = true }) 66 | api.nvim_create_autocmd({ "BufLeave", "CursorMoved" }, { 67 | group = group, 68 | pattern = { "NvimTree_*" }, 69 | callback = function() 70 | hide(M.popup_win) 71 | end, 72 | }) 73 | 74 | api.nvim_create_autocmd({ "CursorMoved" }, { 75 | group = group, 76 | pattern = { "NvimTree_*" }, 77 | callback = function() 78 | show() 79 | end, 80 | }) 81 | end 82 | 83 | return M 84 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/root/change-dir.lua: -------------------------------------------------------------------------------- 1 | local a = vim.api 2 | 3 | local log = require "nvim-tree.log" 4 | local utils = require "nvim-tree.utils" 5 | local core = require "nvim-tree.core" 6 | 7 | local M = { 8 | current_tab = a.nvim_get_current_tabpage(), 9 | } 10 | 11 | local function clean_input_cwd(name) 12 | local root_parent_cwd = vim.fn.fnamemodify(utils.path_remove_trailing(core.get_cwd()), ":h") 13 | if name == ".." and root_parent_cwd then 14 | return vim.fn.expand(root_parent_cwd) 15 | else 16 | return vim.fn.expand(name) 17 | end 18 | end 19 | 20 | local function is_window_event(new_tabpage) 21 | local is_event_scope_window = vim.v.event.scope == "window" or vim.v.event.changed_window 22 | return is_event_scope_window and new_tabpage == M.current_tab 23 | end 24 | 25 | local function prevent_cwd_change(foldername) 26 | local is_same_cwd = foldername == core.get_cwd() 27 | local is_restricted_above = M.options.restrict_above_cwd and foldername < vim.fn.getcwd(-1, -1) 28 | return is_same_cwd or is_restricted_above 29 | end 30 | 31 | function M.fn(input_cwd, with_open) 32 | if not core.get_explorer() then 33 | return 34 | end 35 | 36 | local new_tabpage = a.nvim_get_current_tabpage() 37 | if is_window_event(new_tabpage) then 38 | return 39 | end 40 | 41 | local foldername = clean_input_cwd(input_cwd) 42 | if prevent_cwd_change(foldername) then 43 | return 44 | end 45 | 46 | M.current_tab = new_tabpage 47 | M.force_dirchange(foldername, with_open) 48 | end 49 | 50 | local function cd(global, path) 51 | vim.cmd((global and "cd " or "lcd ") .. vim.fn.fnameescape(path)) 52 | end 53 | 54 | local function should_change_dir() 55 | return M.options.enable and vim.tbl_isempty(vim.v.event) 56 | end 57 | 58 | local function add_profiling_to(f) 59 | return function(foldername, should_open_view) 60 | local ps = log.profile_start("change dir %s", foldername) 61 | f(foldername, should_open_view) 62 | log.profile_end(ps, "change dir %s", foldername) 63 | end 64 | end 65 | 66 | M.force_dirchange = add_profiling_to(function(foldername, should_open_view) 67 | if should_change_dir() then 68 | cd(M.options.global, foldername) 69 | end 70 | 71 | core.init(foldername) 72 | 73 | if should_open_view then 74 | require("nvim-tree.lib").open() 75 | else 76 | require("nvim-tree.renderer").draw() 77 | end 78 | end) 79 | 80 | function M.setup(options) 81 | M.options = options.actions.change_dir 82 | end 83 | 84 | return M 85 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/watch.lua: -------------------------------------------------------------------------------- 1 | local log = require "nvim-tree.log" 2 | local utils = require "nvim-tree.utils" 3 | local git = require "nvim-tree.git" 4 | local Watcher = require("nvim-tree.watcher").Watcher 5 | 6 | local M = {} 7 | 8 | local function reload_and_get_git_project(path) 9 | local project_root = git.get_project_root(path) 10 | git.reload_project(project_root, path) 11 | return project_root, git.get_project(project_root) or {} 12 | end 13 | 14 | local function update_parent_statuses(node, project, root) 15 | while project and node and node.absolute_path ~= root do 16 | require("nvim-tree.explorer.common").update_git_status(node, false, project) 17 | node = node.parent 18 | end 19 | end 20 | 21 | local function is_git(path) 22 | return vim.fn.fnamemodify(path, ":t") == ".git" 23 | end 24 | 25 | local IGNORED_PATHS = { 26 | -- disable watchers on kernel filesystems 27 | -- which have a lot of unwanted events 28 | "/sys", 29 | "/proc", 30 | "/dev", 31 | } 32 | 33 | local function is_folder_ignored(path) 34 | for _, folder in ipairs(IGNORED_PATHS) do 35 | if vim.startswith(path, folder) then 36 | return true 37 | end 38 | end 39 | return false 40 | end 41 | 42 | local function refresh_path(path) 43 | log.line("watcher", "node event executing '%s'", path) 44 | local n = utils.get_node_from_path(path) 45 | if not n then 46 | return 47 | end 48 | 49 | local node = utils.get_parent_of_group(n) 50 | local project_root, project = reload_and_get_git_project(path) 51 | require("nvim-tree.explorer.reload").reload(node, project) 52 | update_parent_statuses(node, project, project_root) 53 | 54 | require("nvim-tree.renderer").draw() 55 | end 56 | 57 | function M.create_watcher(absolute_path) 58 | if not M.enabled then 59 | return nil 60 | end 61 | if is_git(absolute_path) or is_folder_ignored(absolute_path) then 62 | return nil 63 | end 64 | 65 | local function callback(watcher) 66 | log.line("watcher", "node event scheduled %s", watcher.context) 67 | utils.debounce(watcher.context, M.debounce_delay, function() 68 | refresh_path(watcher._path) 69 | end) 70 | end 71 | 72 | M.uid = M.uid + 1 73 | return Watcher:new(absolute_path, callback, { 74 | context = "explorer:watch:" .. absolute_path .. ":" .. M.uid, 75 | }) 76 | end 77 | 78 | function M.setup(opts) 79 | M.enabled = opts.filesystem_watchers.enable 80 | M.debounce_delay = opts.filesystem_watchers.debounce_delay 81 | M.uid = 0 82 | end 83 | 84 | return M 85 | -------------------------------------------------------------------------------- /scripts/generate_default_mappings.lua: -------------------------------------------------------------------------------- 1 | -- luacheck:ignore 113 2 | ---@diagnostic disable: undefined-global 3 | 4 | -- write DEFAULT_MAPPINGS in various formats 5 | 6 | local max_key_help = 0 7 | local max_key_lua = 0 8 | local max_action_help = 0 9 | local outs_help = {} 10 | local outs_lua = {} 11 | 12 | for _, m in pairs(DEFAULT_MAPPINGS) do 13 | local out 14 | if type(m.key) == "table" then 15 | local first = true 16 | local keys_lua = "key = {" 17 | for _, sub_key in pairs(m.key) do 18 | -- lua 19 | keys_lua = string.format('%s%s "%s"', keys_lua, first and "" or ",", sub_key) 20 | 21 | -- help 22 | out = {} 23 | if first then 24 | out.action = m.action 25 | out.desc = m.desc 26 | first = false 27 | else 28 | out.action = "" 29 | out.desc = "" 30 | end 31 | out.key = string.format("`%s`", sub_key) 32 | max_action_help = math.max(#out.action, max_action_help) 33 | max_key_help = math.max(#out.key, max_key_help) 34 | table.insert(outs_help, out) 35 | end 36 | 37 | -- lua 38 | out = {} 39 | out.key = string.format("%s },", keys_lua) 40 | table.insert(outs_lua, out) 41 | else 42 | -- help 43 | out = {} 44 | out.action = m.action 45 | out.desc = m.desc 46 | out.key = string.format("`%s`", m.key) 47 | table.insert(outs_help, out) 48 | max_action_help = math.max(#out.action, max_action_help) 49 | max_key_help = math.max(#out.key, max_key_help) 50 | 51 | -- lua 52 | out = {} 53 | out.key = string.format('key = "%s",', m.key) 54 | table.insert(outs_lua, out) 55 | end 56 | 57 | --lua 58 | out.action = string.format('action = "%s"', m.action) 59 | max_key_lua = math.max(#out.key, max_key_lua) 60 | end 61 | 62 | -- help 63 | local file = io.open("/tmp/DEFAULT_MAPPINGS.help", "w") 64 | io.output(file) 65 | io.write "\n" 66 | local fmt = string.format("%%-%d.%ds %%-%d.%ds %%s\n", max_key_help, max_key_help, max_action_help, max_action_help) 67 | for _, m in pairs(outs_help) do 68 | if m.action == "" then 69 | io.write(string.format("%s\n", m.key)) 70 | else 71 | io.write(string.format(fmt, m.key, m.action, m.desc)) 72 | end 73 | end 74 | io.write "\n" 75 | io.close(file) 76 | 77 | -- lua 78 | file = io.open("/tmp/DEFAULT_MAPPINGS.lua", "w") 79 | io.output(file) 80 | fmt = string.format(" { %%-%d.%ds %%s },\n", max_key_lua, max_key_lua) 81 | for _, m in pairs(outs_lua) do 82 | io.write(string.format(fmt, m.key, m.action)) 83 | end 84 | io.close(file) 85 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/components/icons.lua: -------------------------------------------------------------------------------- 1 | local M = { i = {} } 2 | 3 | local function config_symlinks() 4 | M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink .. M.config.padding or "" 5 | M.i.symlink_arrow = M.config.symlink_arrow 6 | end 7 | 8 | local function empty() 9 | return "" 10 | end 11 | 12 | local function get_folder_icon(open, is_symlink, has_children) 13 | local n 14 | if is_symlink and open then 15 | n = M.config.glyphs.folder.symlink_open 16 | elseif is_symlink then 17 | n = M.config.glyphs.folder.symlink 18 | elseif open then 19 | if has_children then 20 | n = M.config.glyphs.folder.open 21 | else 22 | n = M.config.glyphs.folder.empty_open 23 | end 24 | else 25 | if has_children then 26 | n = M.config.glyphs.folder.default 27 | else 28 | n = M.config.glyphs.folder.empty 29 | end 30 | end 31 | return n .. M.config.padding 32 | end 33 | 34 | local function get_file_icon_default() 35 | local hl_group = "NvimTreeFileIcon" 36 | local icon = M.config.glyphs.default 37 | if #icon > 0 then 38 | return icon .. M.config.padding, hl_group 39 | else 40 | return "" 41 | end 42 | end 43 | 44 | local function get_file_icon_webdev(fname, extension) 45 | local icon, hl_group = M.devicons.get_icon(fname, extension) 46 | if not M.config.webdev_colors then 47 | hl_group = "NvimTreeFileIcon" 48 | end 49 | if icon and hl_group ~= "DevIconDefault" then 50 | return icon .. M.config.padding, hl_group 51 | elseif string.match(extension, "%.(.*)") then 52 | -- If there are more extensions to the file, try to grab the icon for them recursively 53 | return get_file_icon_webdev(fname, string.match(extension, "%.(.*)")) 54 | else 55 | return get_file_icon_default() 56 | end 57 | end 58 | 59 | local function config_file_icon() 60 | if M.config.show.file then 61 | if M.devicons then 62 | M.get_file_icon = get_file_icon_webdev 63 | else 64 | M.get_file_icon = get_file_icon_default 65 | end 66 | else 67 | M.get_file_icon = empty 68 | end 69 | end 70 | 71 | local function config_folder_icon() 72 | if M.config.show.folder then 73 | M.get_folder_icon = get_folder_icon 74 | else 75 | M.get_folder_icon = empty 76 | end 77 | end 78 | 79 | function M.reset_config() 80 | config_symlinks() 81 | config_file_icon() 82 | config_folder_icon() 83 | end 84 | 85 | function M.setup(opts) 86 | M.config = opts.renderer.icons 87 | 88 | M.devicons = pcall(require, "nvim-web-devicons") and require "nvim-web-devicons" 89 | end 90 | 91 | return M 92 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/explore.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local utils = require "nvim-tree.utils" 4 | local builders = require "nvim-tree.explorer.node-builders" 5 | local common = require "nvim-tree.explorer.common" 6 | local sorters = require "nvim-tree.explorer.sorters" 7 | local filters = require "nvim-tree.explorer.filters" 8 | local live_filter = require "nvim-tree.live-filter" 9 | 10 | local M = {} 11 | 12 | local function get_type_from(type_, cwd) 13 | return type_ or (uv.fs_stat(cwd) or {}).type 14 | end 15 | 16 | local function populate_children(handle, cwd, node, status) 17 | local node_ignored = node.git_status == "!!" 18 | local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") 19 | while true do 20 | local name, t = uv.fs_scandir_next(handle) 21 | if not name then 22 | break 23 | end 24 | 25 | local abs = utils.path_join { cwd, name } 26 | t = get_type_from(t, abs) 27 | if 28 | not filters.should_ignore(abs) 29 | and not filters.should_ignore_git(abs, status.files) 30 | and not nodes_by_path[abs] 31 | then 32 | local child = nil 33 | if t == "directory" and uv.fs_access(abs, "R") then 34 | child = builders.folder(node, abs, name) 35 | elseif t == "file" then 36 | child = builders.file(node, abs, name) 37 | elseif t == "link" then 38 | local link = builders.link(node, abs, name) 39 | if link.link_to ~= nil then 40 | child = link 41 | end 42 | end 43 | if child then 44 | table.insert(node.nodes, child) 45 | nodes_by_path[child.absolute_path] = true 46 | common.update_git_status(child, node_ignored, status) 47 | end 48 | end 49 | end 50 | end 51 | 52 | local function get_dir_handle(cwd) 53 | local handle = uv.fs_scandir(cwd) 54 | if type(handle) == "string" then 55 | utils.notify.error(handle) 56 | return 57 | end 58 | return handle 59 | end 60 | 61 | function M.explore(node, status) 62 | local cwd = node.link_to or node.absolute_path 63 | local handle = get_dir_handle(cwd) 64 | if not handle then 65 | return 66 | end 67 | 68 | populate_children(handle, cwd, node, status) 69 | 70 | local is_root = not node.parent 71 | local child_folder_only = common.has_one_child_folder(node) and node.nodes[1] 72 | if M.config.group_empty and not is_root and child_folder_only then 73 | node.group_next = child_folder_only 74 | local ns = M.explore(child_folder_only, status) 75 | node.nodes = ns or {} 76 | return ns 77 | end 78 | 79 | sorters.merge_sort(node.nodes, sorters.node_comparator) 80 | live_filter.apply_filter(node) 81 | return node.nodes 82 | end 83 | 84 | function M.setup(opts) 85 | M.config = opts.renderer 86 | end 87 | 88 | return M 89 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a problem with nvim-tree 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Is this a question? 9 | * Please start a new [Q&A discussion](https://github.com/kyazdani42/nvim-tree.lua/discussions/new) instead of raising a bug. 10 | 11 | Before reporting: 12 | * search [existing issues](https://github.com/kyazdani42/nvim-tree.lua/issues) 13 | * ensure that nvim-tree is updated to the latest version 14 | 15 | If you are experiencing performance issues, please [enable profiling](https://github.com/kyazdani42/nvim-tree.lua#performance-issues) and attach the logs. 16 | - type: textarea 17 | attributes: 18 | label: "Description" 19 | description: "A short description of the problem you are reporting." 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: "Neovim version" 25 | description: "Output of `nvim --version`. Please see nvim-tree.lua [minimum required version](https://github.com/kyazdani42/nvim-tree.lua#notice)." 26 | placeholder: | 27 | NVIM v0.6.1 28 | Build type: Release 29 | LuaJIT 2.1.0-beta3 30 | render: text 31 | validations: 32 | required: true 33 | - type: input 34 | attributes: 35 | label: "Operating system and version" 36 | placeholder: "Linux 5.16.11-arch1-1, macOS 11.5, Windows 10" 37 | validations: 38 | required: true 39 | - type: input 40 | attributes: 41 | label: "nvim-tree version" 42 | description: "`cd /nvim-tree.lua ; git log --format='%h' -n 1`" 43 | placeholder: | 44 | nvim-tree branch, commit or tag number 45 | validations: 46 | required: true 47 | - type: textarea 48 | attributes: 49 | label: "Minimal config" 50 | description: "Minimal(!) configuration necessary to reproduce the issue. 51 | 52 | (Right click) save [nvt-min.lua](https://raw.githubusercontent.com/kyazdani42/nvim-tree.lua/master/.github/ISSUE_TEMPLATE/nvt-min.lua) to `/tmp` and run using `nvim -nu /tmp/nvt-min.lua` 53 | 54 | If _absolutely_ necessary, add plugins and modify the nvim-tree setup at the indicated lines. 55 | 56 | Paste the contents of your `/tmp/nvt-min.lua` here." 57 | render: lua 58 | validations: 59 | required: true 60 | - type: textarea 61 | attributes: 62 | label: "Steps to reproduce" 63 | description: "Steps to reproduce using the minimal config provided below." 64 | placeholder: | 65 | 1. nvim -nu /tmp/nvt-min.lua 66 | 2. :NvimTreeOpen 67 | 3. ... 68 | validations: 69 | required: true 70 | - type: textarea 71 | attributes: 72 | label: "Expected behavior" 73 | description: "A description of the behavior you expected:" 74 | - type: textarea 75 | attributes: 76 | label: "Actual behavior" 77 | description: "Observed behavior (may optionally include images, videos or a screencast)." 78 | 79 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/node-builders.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | local utils = require "nvim-tree.utils" 3 | local watch = require "nvim-tree.explorer.watch" 4 | 5 | local M = { 6 | is_windows = vim.fn.has "win32" == 1, 7 | is_wsl = vim.fn.has "wsl" == 1, 8 | } 9 | 10 | function M.folder(parent, absolute_path, name) 11 | local handle = uv.fs_scandir(absolute_path) 12 | local has_children = handle and uv.fs_scandir_next(handle) ~= nil 13 | 14 | return { 15 | type = "directory", 16 | absolute_path = absolute_path, 17 | fs_stat = uv.fs_stat(absolute_path), 18 | group_next = nil, -- If node is grouped, this points to the next child dir/link node 19 | has_children = has_children, 20 | name = name, 21 | nodes = {}, 22 | open = false, 23 | parent = parent, 24 | watcher = watch.create_watcher(absolute_path), 25 | } 26 | end 27 | 28 | function M.is_executable(parent, absolute_path, ext) 29 | if M.is_windows then 30 | return utils.is_windows_exe(ext) 31 | elseif M.is_wsl then 32 | if parent.is_wsl_windows_fs_path == nil then 33 | -- Evaluate lazily when needed and do so only once for each parent 34 | -- as 'wslpath' calls can get expensive in highly populated directories. 35 | parent.is_wsl_windows_fs_path = utils.is_wsl_windows_fs_path(absolute_path) 36 | end 37 | 38 | if parent.is_wsl_windows_fs_path then 39 | return utils.is_wsl_windows_fs_exe(ext) 40 | end 41 | end 42 | return uv.fs_access(absolute_path, "X") 43 | end 44 | 45 | function M.file(parent, absolute_path, name) 46 | local ext = string.match(name, ".?[^.]+%.(.*)") or "" 47 | 48 | return { 49 | type = "file", 50 | absolute_path = absolute_path, 51 | executable = M.is_executable(parent, absolute_path, ext), 52 | extension = ext, 53 | fs_stat = uv.fs_stat(absolute_path), 54 | name = name, 55 | parent = parent, 56 | } 57 | end 58 | 59 | -- TODO-INFO: sometimes fs_realpath returns nil 60 | -- I expect this be a bug in glibc, because it fails to retrieve the path for some 61 | -- links (for instance libr2.so in /usr/lib) and thus even with a C program realpath fails 62 | -- when it has no real reason to. Maybe there is a reason, but errno is definitely wrong. 63 | -- So we need to check for link_to ~= nil when adding new links to the main tree 64 | function M.link(parent, absolute_path, name) 65 | --- I dont know if this is needed, because in my understanding, there isn't hard links in windows, but just to be sure i changed it. 66 | local link_to = uv.fs_realpath(absolute_path) 67 | local open, nodes, has_children, watcher 68 | if (link_to ~= nil) and uv.fs_stat(link_to).type == "directory" then 69 | local handle = uv.fs_scandir(link_to) 70 | has_children = handle and uv.fs_scandir_next(handle) ~= nil 71 | open = false 72 | nodes = {} 73 | watcher = watch.create_watcher(link_to) 74 | end 75 | 76 | return { 77 | type = "link", 78 | absolute_path = absolute_path, 79 | fs_stat = uv.fs_stat(absolute_path), 80 | group_next = nil, -- If node is grouped, this points to the next child dir/link node 81 | has_children = has_children, 82 | link_to = link_to, 83 | name = name, 84 | nodes = nodes, 85 | open = open, 86 | parent = parent, 87 | watcher = watcher, 88 | } 89 | end 90 | 91 | return M 92 | -------------------------------------------------------------------------------- /lua/nvim-tree/events.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | 3 | local M = {} 4 | 5 | local global_handlers = {} 6 | 7 | M.Event = { 8 | Ready = "Ready", 9 | NodeRenamed = "NodeRenamed", 10 | TreeOpen = "TreeOpen", 11 | TreeClose = "TreeClose", 12 | FileCreated = "FileCreated", 13 | FileRemoved = "FileRemoved", 14 | FolderCreated = "FolderCreated", 15 | FolderRemoved = "FolderRemoved", 16 | Resize = "Resize", 17 | } 18 | 19 | local function get_handlers(event_name) 20 | return global_handlers[event_name] or {} 21 | end 22 | 23 | function M.subscribe(event_name, handler) 24 | local handlers = get_handlers(event_name) 25 | table.insert(handlers, handler) 26 | global_handlers[event_name] = handlers 27 | end 28 | 29 | local function dispatch(event_name, payload) 30 | for _, handler in pairs(get_handlers(event_name)) do 31 | local success, error = pcall(handler, payload) 32 | if not success then 33 | utils.notify.error("Handler for event " .. event_name .. " errored. " .. vim.inspect(error)) 34 | end 35 | end 36 | end 37 | 38 | --@private 39 | function M._dispatch_ready() 40 | dispatch(M.Event.Ready) 41 | end 42 | 43 | --@private 44 | function M._dispatch_node_renamed(old_name, new_name) 45 | dispatch(M.Event.NodeRenamed, { old_name = old_name, new_name = new_name }) 46 | end 47 | 48 | --@private 49 | function M._dispatch_file_removed(fname) 50 | dispatch(M.Event.FileRemoved, { fname = fname }) 51 | end 52 | 53 | --@private 54 | function M._dispatch_file_created(fname) 55 | dispatch(M.Event.FileCreated, { fname = fname }) 56 | end 57 | 58 | --@private 59 | function M._dispatch_folder_created(folder_name) 60 | dispatch(M.Event.FolderCreated, { folder_name = folder_name }) 61 | end 62 | 63 | --@private 64 | function M._dispatch_folder_removed(folder_name) 65 | dispatch(M.Event.FolderRemoved, { folder_name = folder_name }) 66 | end 67 | 68 | --@private 69 | function M._dispatch_on_tree_open() 70 | dispatch(M.Event.TreeOpen, nil) 71 | end 72 | 73 | --@private 74 | function M._dispatch_on_tree_close() 75 | dispatch(M.Event.TreeClose, nil) 76 | end 77 | 78 | --@private 79 | function M._dispatch_on_tree_resize(size) 80 | dispatch(M.Event.Resize, size) 81 | end 82 | 83 | --- @deprecated 84 | function M.on_nvim_tree_ready(handler) 85 | M.subscribe(M.Event.Ready, handler) 86 | end 87 | 88 | --- @deprecated 89 | function M.on_node_renamed(handler) 90 | M.subscribe(M.Event.NodeRenamed, handler) 91 | end 92 | 93 | --- @deprecated 94 | function M.on_file_created(handler) 95 | M.subscribe(M.Event.FileCreated, handler) 96 | end 97 | 98 | --- @deprecated 99 | function M.on_file_removed(handler) 100 | M.subscribe(M.Event.FileRemoved, handler) 101 | end 102 | 103 | --- @deprecated 104 | function M.on_folder_created(handler) 105 | M.subscribe(M.Event.FolderCreated, handler) 106 | end 107 | 108 | --- @deprecated 109 | function M.on_folder_removed(handler) 110 | M.subscribe(M.Event.FolderRemoved, handler) 111 | end 112 | 113 | --- @deprecated 114 | function M.on_tree_open(handler) 115 | M.subscribe(M.Event.TreeOpen, handler) 116 | end 117 | 118 | --- @deprecated 119 | function M.on_tree_close(handler) 120 | M.subscribe(M.Event.TreeClose, handler) 121 | end 122 | 123 | --- @deprecated 124 | function M.on_tree_resize(handler) 125 | M.subscribe(M.Event.Resize, handler) 126 | end 127 | 128 | return M 129 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/fs/remove-file.lua: -------------------------------------------------------------------------------- 1 | local a = vim.api 2 | local luv = vim.loop 3 | 4 | local utils = require "nvim-tree.utils" 5 | local events = require "nvim-tree.events" 6 | local view = require "nvim-tree.view" 7 | local lib = require "nvim-tree.lib" 8 | 9 | local M = {} 10 | 11 | local function close_windows(windows) 12 | if view.View.float.enable and #a.nvim_list_wins() == 1 then 13 | return 14 | end 15 | 16 | for _, window in ipairs(windows) do 17 | if a.nvim_win_is_valid(window) then 18 | a.nvim_win_close(window, true) 19 | end 20 | end 21 | end 22 | 23 | local function clear_buffer(absolute_path) 24 | local bufs = vim.fn.getbufinfo { bufloaded = 1, buflisted = 1 } 25 | for _, buf in pairs(bufs) do 26 | if buf.name == absolute_path then 27 | if buf.hidden == 0 and (#bufs > 1 or view.View.float.enable) then 28 | local winnr = a.nvim_get_current_win() 29 | a.nvim_set_current_win(buf.windows[1]) 30 | vim.cmd ":bn" 31 | if not view.View.float.enable then 32 | a.nvim_set_current_win(winnr) 33 | end 34 | end 35 | a.nvim_buf_delete(buf.bufnr, { force = true }) 36 | if M.close_window then 37 | close_windows(buf.windows) 38 | end 39 | return 40 | end 41 | end 42 | end 43 | 44 | local function remove_dir(cwd) 45 | local handle = luv.fs_scandir(cwd) 46 | if type(handle) == "string" then 47 | return utils.notify.error(handle) 48 | end 49 | 50 | while true do 51 | local name, t = luv.fs_scandir_next(handle) 52 | if not name then 53 | break 54 | end 55 | 56 | local new_cwd = utils.path_join { cwd, name } 57 | if t == "directory" then 58 | local success = remove_dir(new_cwd) 59 | if not success then 60 | return false 61 | end 62 | else 63 | local success = luv.fs_unlink(new_cwd) 64 | if not success then 65 | return false 66 | end 67 | clear_buffer(new_cwd) 68 | end 69 | end 70 | 71 | return luv.fs_rmdir(cwd) 72 | end 73 | 74 | function M.fn(node) 75 | if node.name == ".." then 76 | return 77 | end 78 | local prompt_select = "Remove " .. node.name .. " ?" 79 | local prompt_input = prompt_select .. " y/n: " 80 | lib.prompt(prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }, function(item_short) 81 | utils.clear_prompt() 82 | if item_short == "y" then 83 | if node.nodes ~= nil and not node.link_to then 84 | local success = remove_dir(node.absolute_path) 85 | if not success then 86 | return utils.notify.error("Could not remove " .. node.name) 87 | end 88 | events._dispatch_folder_removed(node.absolute_path) 89 | else 90 | local success = luv.fs_unlink(node.absolute_path) 91 | if not success then 92 | return utils.notify.error("Could not remove " .. node.name) 93 | end 94 | events._dispatch_file_removed(node.absolute_path) 95 | clear_buffer(node.absolute_path) 96 | end 97 | utils.notify.info(node.absolute_path .. " was properly removed.") 98 | if M.enable_reload then 99 | require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 100 | end 101 | end 102 | end) 103 | end 104 | 105 | function M.setup(opts) 106 | M.enable_reload = not opts.filesystem_watchers.enable 107 | M.close_window = opts.actions.remove_file.close_window 108 | end 109 | 110 | return M 111 | -------------------------------------------------------------------------------- /lua/nvim-tree/colors.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | 3 | local M = {} 4 | 5 | local function get_color_from_hl(hl_name, fallback) 6 | local id = vim.api.nvim_get_hl_id_by_name(hl_name) 7 | if not id then 8 | return fallback 9 | end 10 | 11 | local foreground = vim.fn.synIDattr(vim.fn.synIDtrans(id), "fg") 12 | if not foreground or foreground == "" then 13 | return fallback 14 | end 15 | 16 | return foreground 17 | end 18 | 19 | local function get_colors() 20 | return { 21 | red = vim.g.terminal_color_1 or get_color_from_hl("Keyword", "Red"), 22 | green = vim.g.terminal_color_2 or get_color_from_hl("Character", "Green"), 23 | yellow = vim.g.terminal_color_3 or get_color_from_hl("PreProc", "Yellow"), 24 | blue = vim.g.terminal_color_4 or get_color_from_hl("Include", "Blue"), 25 | purple = vim.g.terminal_color_5 or get_color_from_hl("Define", "Purple"), 26 | cyan = vim.g.terminal_color_6 or get_color_from_hl("Conditional", "Cyan"), 27 | dark_red = vim.g.terminal_color_9 or get_color_from_hl("Keyword", "DarkRed"), 28 | orange = vim.g.terminal_color_11 or get_color_from_hl("Number", "Orange"), 29 | } 30 | end 31 | 32 | local function get_hl_groups() 33 | local colors = get_colors() 34 | 35 | return { 36 | IndentMarker = { fg = "#8094b4" }, 37 | Symlink = { gui = "bold", fg = colors.cyan }, 38 | FolderIcon = { fg = "#8094b4" }, 39 | RootFolder = { fg = colors.purple }, 40 | 41 | ExecFile = { gui = "bold", fg = colors.green }, 42 | SpecialFile = { gui = "bold,underline", fg = colors.yellow }, 43 | ImageFile = { gui = "bold", fg = colors.purple }, 44 | OpenedFile = { gui = "bold", fg = colors.green }, 45 | 46 | GitDirty = { fg = colors.dark_red }, 47 | GitDeleted = { fg = colors.dark_red }, 48 | GitStaged = { fg = colors.green }, 49 | GitMerge = { fg = colors.orange }, 50 | GitRenamed = { fg = colors.purple }, 51 | GitNew = { fg = colors.yellow }, 52 | 53 | WindowPicker = { gui = "bold", fg = "#ededed", bg = "#4493c8" }, 54 | LiveFilterPrefix = { gui = "bold", fg = colors.purple }, 55 | LiveFilterValue = { gui = "bold", fg = "#fff" }, 56 | 57 | Bookmark = { fg = colors.green }, 58 | } 59 | end 60 | 61 | local function get_links() 62 | return { 63 | FolderName = "Directory", 64 | EmptyFolderName = "Directory", 65 | OpenedFolderName = "Directory", 66 | Normal = "Normal", 67 | NormalNC = "NvimTreeNormal", 68 | EndOfBuffer = "EndOfBuffer", 69 | CursorLine = "CursorLine", 70 | VertSplit = "VertSplit", 71 | WinSeparator = "NvimTreeVertSplit", 72 | CursorColumn = "CursorColumn", 73 | FileDirty = "NvimTreeGitDirty", 74 | FileNew = "NvimTreeGitNew", 75 | FileRenamed = "NvimTreeGitRenamed", 76 | FileMerge = "NvimTreeGitMerge", 77 | FileStaged = "NvimTreeGitStaged", 78 | FileDeleted = "NvimTreeGitDeleted", 79 | FileIgnored = "NvimTreeGitIgnored", 80 | Popup = "Normal", 81 | GitIgnored = "Comment", 82 | StatusLine = "StatusLine", 83 | StatusLineNC = "StatusLineNC", 84 | SignColumn = "NvimTreeNormal", 85 | } 86 | end 87 | 88 | function M.setup() 89 | local higlight_groups = get_hl_groups() 90 | for k, d in pairs(higlight_groups) do 91 | local gui = d.gui and " gui=" .. d.gui or "" 92 | local fg = d.fg and " guifg=" .. d.fg or "" 93 | local bg = d.bg and " guibg=" .. d.bg or "" 94 | api.nvim_command("hi def NvimTree" .. k .. gui .. fg .. bg) 95 | end 96 | 97 | local links = get_links() 98 | for k, d in pairs(links) do 99 | api.nvim_command("hi def link NvimTree" .. k .. " " .. d) 100 | end 101 | end 102 | 103 | return M 104 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/fs/trash.lua: -------------------------------------------------------------------------------- 1 | local a = vim.api 2 | 3 | local lib = require "nvim-tree.lib" 4 | 5 | local M = { 6 | config = { 7 | is_windows = vim.fn.has "win32" == 1 or vim.fn.has "win32unix" == 1, 8 | is_macos = vim.fn.has "mac" == 1 or vim.fn.has "macunix" == 1, 9 | is_unix = vim.fn.has "unix" == 1, 10 | }, 11 | } 12 | 13 | local utils = require "nvim-tree.utils" 14 | local events = require "nvim-tree.events" 15 | 16 | local function clear_buffer(absolute_path) 17 | local bufs = vim.fn.getbufinfo { bufloaded = 1, buflisted = 1 } 18 | for _, buf in pairs(bufs) do 19 | if buf.name == absolute_path then 20 | if buf.hidden == 0 and #bufs > 1 then 21 | local winnr = a.nvim_get_current_win() 22 | a.nvim_set_current_win(buf.windows[1]) 23 | vim.cmd ":bn" 24 | a.nvim_set_current_win(winnr) 25 | end 26 | vim.api.nvim_buf_delete(buf.bufnr, {}) 27 | return 28 | end 29 | end 30 | end 31 | 32 | function M.fn(node) 33 | if node.name == ".." then 34 | return 35 | end 36 | 37 | -- configs 38 | if M.config.is_unix then 39 | if M.config.trash.cmd == nil then 40 | M.config.trash.cmd = "trash" 41 | end 42 | if M.config.trash.require_confirm == nil then 43 | M.config.trash.require_confirm = true 44 | end 45 | else 46 | utils.notify.warn "Trash is currently a UNIX only feature!" 47 | return 48 | end 49 | 50 | local binary = M.config.trash.cmd:gsub(" .*$", "") 51 | if vim.fn.executable(binary) == 0 then 52 | utils.notify.warn(binary .. " is not executable.") 53 | return 54 | end 55 | 56 | local err_msg = "" 57 | local function on_stderr(_, data) 58 | err_msg = err_msg .. (data and table.concat(data, " ")) 59 | end 60 | 61 | -- trashes a path (file or folder) 62 | local function trash_path(on_exit) 63 | vim.fn.jobstart(M.config.trash.cmd .. ' "' .. node.absolute_path .. '"', { 64 | detach = true, 65 | on_exit = on_exit, 66 | on_stderr = on_stderr, 67 | }) 68 | end 69 | 70 | local function do_trash() 71 | if node.nodes ~= nil and not node.link_to then 72 | trash_path(function(_, rc) 73 | if rc ~= 0 then 74 | utils.notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") 75 | return 76 | end 77 | events._dispatch_folder_removed(node.absolute_path) 78 | if M.enable_reload then 79 | require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 80 | end 81 | end) 82 | else 83 | trash_path(function(_, rc) 84 | if rc ~= 0 then 85 | utils.notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") 86 | return 87 | end 88 | events._dispatch_file_removed(node.absolute_path) 89 | clear_buffer(node.absolute_path) 90 | if M.enable_reload then 91 | require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 92 | end 93 | end) 94 | end 95 | end 96 | 97 | if M.config.trash.require_confirm then 98 | local prompt_select = "Trash " .. node.name .. " ?" 99 | local prompt_input = prompt_select .. " y/n: " 100 | lib.prompt(prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }, function(item_short) 101 | utils.clear_prompt() 102 | if item_short == "y" then 103 | do_trash() 104 | end 105 | end) 106 | else 107 | do_trash() 108 | end 109 | end 110 | 111 | function M.setup(opts) 112 | M.config.trash = opts.trash or {} 113 | M.enable_reload = not opts.filesystem_watchers.enable 114 | end 115 | 116 | return M 117 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/init.lua: -------------------------------------------------------------------------------- 1 | local core = require "nvim-tree.core" 2 | local diagnostics = require "nvim-tree.diagnostics" 3 | local log = require "nvim-tree.log" 4 | local view = require "nvim-tree.view" 5 | 6 | local _padding = require "nvim-tree.renderer.components.padding" 7 | local icon_component = require "nvim-tree.renderer.components.icons" 8 | local full_name = require "nvim-tree.renderer.components.full-name" 9 | local help = require "nvim-tree.renderer.help" 10 | local git = require "nvim-tree.renderer.components.git" 11 | local Builder = require "nvim-tree.renderer.builder" 12 | local live_filter = require "nvim-tree.live-filter" 13 | local marks = require "nvim-tree.marks" 14 | 15 | local api = vim.api 16 | 17 | local M = { 18 | last_highlights = {}, 19 | } 20 | 21 | local namespace_id = api.nvim_create_namespace "NvimTreeHighlights" 22 | 23 | local function _draw(bufnr, lines, hl, signs) 24 | api.nvim_buf_set_option(bufnr, "modifiable", true) 25 | api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) 26 | M.render_hl(bufnr, hl) 27 | api.nvim_buf_set_option(bufnr, "modifiable", false) 28 | vim.fn.sign_unplace(git.SIGN_GROUP) 29 | for _, sign in pairs(signs) do 30 | vim.fn.sign_place(0, git.SIGN_GROUP, sign.sign, bufnr, { lnum = sign.lnum, priority = 1 }) 31 | end 32 | end 33 | 34 | function M.render_hl(bufnr, hl) 35 | if not bufnr or not api.nvim_buf_is_loaded(bufnr) then 36 | return 37 | end 38 | api.nvim_buf_clear_namespace(bufnr, namespace_id, 0, -1) 39 | for _, data in ipairs(hl or M.last_highlights) do 40 | api.nvim_buf_add_highlight(bufnr, namespace_id, data[1], data[2], data[3], data[4]) 41 | end 42 | end 43 | 44 | local picture_map = { 45 | jpg = true, 46 | jpeg = true, 47 | png = true, 48 | gif = true, 49 | } 50 | 51 | function M.draw() 52 | local bufnr = view.get_bufnr() 53 | if not core.get_explorer() or not bufnr or not api.nvim_buf_is_loaded(bufnr) then 54 | return 55 | end 56 | 57 | local ps = log.profile_start "draw" 58 | 59 | local cursor = api.nvim_win_get_cursor(view.get_winnr()) 60 | icon_component.reset_config() 61 | 62 | local lines, hl 63 | local signs = {} 64 | if view.is_help_ui() then 65 | lines, hl = help.compute_lines() 66 | else 67 | lines, hl, signs = Builder.new(core.get_cwd()) 68 | :configure_root_modifier(M.config.root_folder_modifier) 69 | :configure_trailing_slash(M.config.add_trailing) 70 | :configure_special_files(M.config.special_files) 71 | :configure_picture_map(picture_map) 72 | :configure_opened_file_highlighting(M.config.highlight_opened_files) 73 | :configure_git_icons_padding(M.config.icons.padding) 74 | :configure_git_icons_placement(M.config.icons.git_placement) 75 | :configure_symlink_destination(M.config.symlink_destination) 76 | :configure_filter(live_filter.filter, live_filter.prefix) 77 | :build_header(view.is_root_folder_visible(core.get_cwd())) 78 | :build(core.get_explorer()) 79 | :unwrap() 80 | end 81 | 82 | _draw(bufnr, lines, hl, signs) 83 | 84 | M.last_highlights = hl 85 | 86 | if cursor and #lines >= cursor[1] then 87 | api.nvim_win_set_cursor(view.get_winnr(), cursor) 88 | end 89 | 90 | if view.is_help_ui() then 91 | diagnostics.clear() 92 | marks.clear() 93 | else 94 | diagnostics.update() 95 | marks.draw() 96 | end 97 | 98 | view.grow_from_content() 99 | 100 | log.profile_end(ps, "draw") 101 | end 102 | 103 | function M.setup(opts) 104 | M.config = opts.renderer 105 | 106 | _padding.setup(opts) 107 | full_name.setup(opts) 108 | git.setup(opts) 109 | icon_component.setup(opts) 110 | end 111 | 112 | return M 113 | -------------------------------------------------------------------------------- /lua/nvim-tree/watcher.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local log = require "nvim-tree.log" 4 | local utils = require "nvim-tree.utils" 5 | 6 | local M = {} 7 | 8 | local Event = { 9 | _events = {}, 10 | } 11 | Event.__index = Event 12 | 13 | local Watcher = { 14 | _watchers = {}, 15 | } 16 | Watcher.__index = Watcher 17 | 18 | local FS_EVENT_FLAGS = { 19 | -- inotify or equivalent will be used; fallback to stat has not yet been implemented 20 | stat = false, 21 | -- recursive is not functional in neovim's libuv implementation 22 | recursive = false, 23 | } 24 | 25 | function Event:new(path) 26 | log.line("watcher", "Event:new '%s'", path) 27 | 28 | local e = setmetatable({ 29 | _path = path, 30 | _fs_event = nil, 31 | _listeners = {}, 32 | }, Event) 33 | 34 | if e:start() then 35 | Event._events[path] = e 36 | return e 37 | else 38 | return nil 39 | end 40 | end 41 | 42 | function Event:start() 43 | log.line("watcher", "Event:start '%s'", self._path) 44 | 45 | local rc, _, name 46 | 47 | self._fs_event, _, name = uv.new_fs_event() 48 | if not self._fs_event then 49 | self._fs_event = nil 50 | utils.notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name)) 51 | return false 52 | end 53 | 54 | local event_cb = vim.schedule_wrap(function(err, filename) 55 | if err then 56 | log.line("watcher", "event_cb for %s fail : %s", self._path, err) 57 | else 58 | log.line("watcher", "event_cb '%s' '%s'", self._path, filename) 59 | for _, listener in ipairs(self._listeners) do 60 | listener() 61 | end 62 | end 63 | end) 64 | 65 | rc, _, name = self._fs_event:start(self._path, FS_EVENT_FLAGS, event_cb) 66 | if rc ~= 0 then 67 | utils.notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self._path, name)) 68 | return false 69 | end 70 | 71 | return true 72 | end 73 | 74 | function Event:add(listener) 75 | table.insert(self._listeners, listener) 76 | end 77 | 78 | function Event:remove(listener) 79 | utils.array_remove(self._listeners, listener) 80 | if #self._listeners == 0 then 81 | self:destroy() 82 | end 83 | end 84 | 85 | function Event:destroy() 86 | log.line("watcher", "Event:destroy '%s'", self._path) 87 | 88 | if self._fs_event then 89 | local rc, _, name = self._fs_event:stop() 90 | if rc ~= 0 then 91 | utils.notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self._path, name)) 92 | end 93 | self._fs_event = nil 94 | end 95 | 96 | Event._events[self._path] = nil 97 | end 98 | 99 | function Watcher:new(path, callback, data) 100 | log.line("watcher", "Watcher:new '%s'", path) 101 | 102 | local w = setmetatable(data, Watcher) 103 | 104 | w._event = Event._events[path] or Event:new(path) 105 | w._listener = nil 106 | w._path = path 107 | w._callback = callback 108 | 109 | if not w._event then 110 | return nil 111 | end 112 | 113 | w:start() 114 | 115 | table.insert(Watcher._watchers, w) 116 | 117 | return w 118 | end 119 | 120 | function Watcher:start() 121 | self._listener = function() 122 | self._callback(self) 123 | end 124 | 125 | self._event:add(self._listener) 126 | end 127 | 128 | function Watcher:destroy() 129 | log.line("watcher", "Watcher:destroy '%s'", self._path) 130 | 131 | self._event:remove(self._listener) 132 | 133 | utils.array_remove(Watcher._watchers, self) 134 | end 135 | 136 | M.Watcher = Watcher 137 | 138 | function M.purge_watchers() 139 | log.line("watcher", "purge_watchers") 140 | 141 | for _, w in ipairs(utils.array_shallow_clone(Watcher._watchers)) do 142 | w:destroy() 143 | end 144 | 145 | for _, e in pairs(Event._events) do 146 | e:destroy() 147 | end 148 | end 149 | 150 | return M 151 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/components/padding.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local function check_siblings_for_folder(node, with_arrows) 4 | if with_arrows then 5 | local has_files = false 6 | local has_folders = false 7 | for _, n in pairs(node.parent.nodes) do 8 | if n.nodes and node.absolute_path ~= n.absolute_path then 9 | has_folders = true 10 | end 11 | if not n.nodes then 12 | has_files = true 13 | end 14 | if has_files and has_folders then 15 | return true 16 | end 17 | end 18 | end 19 | return false 20 | end 21 | 22 | local function get_padding_indent_markers(depth, idx, nodes_number, markers, with_arrows, inline_arrows, node) 23 | local base_padding = with_arrows and (not node.nodes or depth > 0) and " " or "" 24 | local padding = (inline_arrows or depth == 0) and base_padding or "" 25 | 26 | if depth > 0 then 27 | local has_folder_sibling = check_siblings_for_folder(node, with_arrows) 28 | local indent = string.rep(" ", M.config.indent_width - 1) 29 | markers[depth] = idx ~= nodes_number 30 | for i = 1, depth do 31 | local glyph 32 | if idx == nodes_number and i == depth then 33 | local bottom_width = M.config.indent_width 34 | - 2 35 | + (with_arrows and not inline_arrows and has_folder_sibling and 2 or 0) 36 | glyph = M.config.indent_markers.icons.corner 37 | .. string.rep(M.config.indent_markers.icons.bottom, bottom_width) 38 | .. (M.config.indent_width > 1 and " " or "") 39 | elseif markers[i] and i == depth then 40 | glyph = M.config.indent_markers.icons.item .. indent 41 | elseif markers[i] then 42 | glyph = M.config.indent_markers.icons.edge .. indent 43 | else 44 | glyph = M.config.indent_markers.icons.none .. indent 45 | end 46 | 47 | if not with_arrows or (inline_arrows and (depth ~= i or not node.nodes)) then 48 | padding = padding .. glyph 49 | elseif inline_arrows then 50 | padding = padding 51 | elseif idx ~= nodes_number and depth == i and not node.nodes and has_folder_sibling then 52 | padding = padding .. base_padding .. glyph .. base_padding 53 | else 54 | padding = padding .. base_padding .. glyph 55 | end 56 | end 57 | end 58 | return padding 59 | end 60 | 61 | local function get_padding_arrows(node, indent) 62 | if node.nodes then 63 | return M.config.icons.glyphs.folder[node.open and "arrow_open" or "arrow_closed"] .. " " 64 | elseif indent then 65 | return " " 66 | else 67 | return "" 68 | end 69 | end 70 | 71 | function M.get_padding(depth, idx, nodes_number, node, markers) 72 | local padding = "" 73 | 74 | local show_arrows = M.config.icons.show.folder_arrow 75 | local show_markers = M.config.indent_markers.enable 76 | local inline_arrows = M.config.indent_markers.inline_arrows 77 | local indent_width = M.config.indent_width 78 | 79 | if show_markers then 80 | padding = padding .. get_padding_indent_markers(depth, idx, nodes_number, markers, show_arrows, inline_arrows, node) 81 | else 82 | padding = padding .. string.rep(" ", depth * indent_width) 83 | end 84 | 85 | if show_arrows then 86 | padding = padding .. get_padding_arrows(node, not show_markers) 87 | end 88 | 89 | return padding 90 | end 91 | 92 | function M.setup(opts) 93 | M.config = opts.renderer 94 | 95 | if M.config.indent_width < 1 then 96 | M.config.indent_width = 1 97 | end 98 | 99 | local function check_marker(symbol) 100 | if #symbol == 0 then 101 | return " " 102 | end 103 | -- return the first character from the UTF-8 encoded string; we may use utf8.codes from Lua 5.3 when available 104 | return symbol:match "[%z\1-\127\194-\244][\128-\191]*" 105 | end 106 | 107 | for k, v in pairs(M.config.indent_markers.icons) do 108 | M.config.indent_markers.icons[k] = check_marker(v) 109 | end 110 | end 111 | 112 | return M 113 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/reload.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local utils = require "nvim-tree.utils" 4 | local builders = require "nvim-tree.explorer.node-builders" 5 | local common = require "nvim-tree.explorer.common" 6 | local filters = require "nvim-tree.explorer.filters" 7 | local sorters = require "nvim-tree.explorer.sorters" 8 | local live_filter = require "nvim-tree.live-filter" 9 | 10 | local M = {} 11 | 12 | local function update_status(nodes_by_path, node_ignored, status) 13 | return function(node) 14 | if nodes_by_path[node.absolute_path] then 15 | common.update_git_status(node, node_ignored, status) 16 | end 17 | return node 18 | end 19 | end 20 | 21 | function M.reload(node, status) 22 | local cwd = node.link_to or node.absolute_path 23 | local handle = uv.fs_scandir(cwd) 24 | if type(handle) == "string" then 25 | utils.notify.error(handle) 26 | return 27 | end 28 | 29 | if node.group_next then 30 | node.nodes = { node.group_next } 31 | node.group_next = nil 32 | end 33 | 34 | local child_names = {} 35 | 36 | local node_ignored = node.git_status == "!!" 37 | local nodes_by_path = utils.key_by(node.nodes, "absolute_path") 38 | while true do 39 | local ok, name, t = pcall(uv.fs_scandir_next, handle) 40 | if not ok or not name then 41 | break 42 | end 43 | 44 | local stat 45 | local function fs_stat_cached(path) 46 | if stat ~= nil then 47 | return stat 48 | end 49 | 50 | stat = uv.fs_stat(path) 51 | return stat 52 | end 53 | 54 | local abs = utils.path_join { cwd, name } 55 | t = t or (fs_stat_cached(abs) or {}).type 56 | if not filters.should_ignore(abs) and not filters.should_ignore_git(abs, status.files) then 57 | child_names[abs] = true 58 | 59 | -- Recreate node if type changes. 60 | if nodes_by_path[abs] then 61 | local n = nodes_by_path[abs] 62 | 63 | if n.type ~= t then 64 | utils.array_remove(node.nodes, n) 65 | common.node_destroy(n) 66 | nodes_by_path[abs] = nil 67 | end 68 | end 69 | 70 | if not nodes_by_path[abs] then 71 | if t == "directory" and uv.fs_access(abs, "R") then 72 | local folder = builders.folder(node, abs, name) 73 | nodes_by_path[abs] = folder 74 | table.insert(node.nodes, folder) 75 | elseif t == "file" then 76 | local file = builders.file(node, abs, name) 77 | nodes_by_path[abs] = file 78 | table.insert(node.nodes, file) 79 | elseif t == "link" then 80 | local link = builders.link(node, abs, name) 81 | if link.link_to ~= nil then 82 | nodes_by_path[abs] = link 83 | table.insert(node.nodes, link) 84 | end 85 | end 86 | else 87 | local n = nodes_by_path[abs] 88 | if n then 89 | n.executable = builders.is_executable(n.parent, abs, n.extension or "") 90 | n.fs_stat = fs_stat_cached(abs) 91 | end 92 | end 93 | end 94 | end 95 | 96 | node.nodes = vim.tbl_map( 97 | update_status(nodes_by_path, node_ignored, status), 98 | vim.tbl_filter(function(n) 99 | if child_names[n.absolute_path] then 100 | return child_names[n.absolute_path] 101 | else 102 | common.node_destroy(n) 103 | return nil 104 | end 105 | end, node.nodes) 106 | ) 107 | 108 | local is_root = not node.parent 109 | local child_folder_only = common.has_one_child_folder(node) and node.nodes[1] 110 | if M.config.group_empty and not is_root and child_folder_only then 111 | node.group_next = child_folder_only 112 | local ns = M.reload(child_folder_only, status) 113 | node.nodes = ns or {} 114 | return ns 115 | end 116 | 117 | sorters.merge_sort(node.nodes, sorters.node_comparator) 118 | live_filter.apply_filter(node) 119 | return node.nodes 120 | end 121 | 122 | function M.setup(opts) 123 | M.config = opts.renderer 124 | end 125 | 126 | return M 127 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/fs/create-file.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local utils = require "nvim-tree.utils" 4 | local events = require "nvim-tree.events" 5 | local lib = require "nvim-tree.lib" 6 | local core = require "nvim-tree.core" 7 | 8 | local M = {} 9 | 10 | local function create_and_notify(file) 11 | local ok, fd = pcall(uv.fs_open, file, "w", 420) 12 | if not ok then 13 | utils.notify.error("Couldn't create file " .. file) 14 | return 15 | end 16 | uv.fs_close(fd) 17 | events._dispatch_file_created(file) 18 | end 19 | 20 | local function create_file(file) 21 | if utils.file_exists(file) then 22 | local prompt_select = "Overwrite " .. file .. " ?" 23 | local prompt_input = prompt_select .. " y/n: " 24 | lib.prompt(prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }, function(item_short) 25 | utils.clear_prompt() 26 | if item_short == "y" then 27 | create_and_notify(file) 28 | end 29 | end) 30 | else 31 | create_and_notify(file) 32 | end 33 | end 34 | 35 | local function get_num_nodes(iter) 36 | local i = 0 37 | for _ in iter do 38 | i = i + 1 39 | end 40 | return i 41 | end 42 | 43 | local function get_containing_folder(node) 44 | local is_open = M.create_in_closed_folder or node.open 45 | if node.nodes ~= nil and is_open then 46 | return utils.path_add_trailing(node.absolute_path) 47 | end 48 | local node_name_size = #(node.name or "") 49 | return node.absolute_path:sub(0, -node_name_size - 1) 50 | end 51 | 52 | function M.fn(node) 53 | node = node and lib.get_last_group_node(node) 54 | if not node or node.name == ".." then 55 | node = { 56 | absolute_path = core.get_cwd(), 57 | nodes = core.get_explorer().nodes, 58 | open = true, 59 | } 60 | end 61 | 62 | local containing_folder = get_containing_folder(node) 63 | 64 | local input_opts = { prompt = "Create file ", default = containing_folder, completion = "file" } 65 | 66 | vim.ui.input(input_opts, function(new_file_path) 67 | utils.clear_prompt() 68 | if not new_file_path or new_file_path == containing_folder then 69 | return 70 | end 71 | 72 | if utils.file_exists(new_file_path) then 73 | utils.notify.warn "Cannot create: file already exists" 74 | return 75 | end 76 | 77 | -- create a folder for each path element if the folder does not exist 78 | -- if the answer ends with a /, create a file for the last path element 79 | local is_last_path_file = not new_file_path:match(utils.path_separator .. "$") 80 | local path_to_create = "" 81 | local idx = 0 82 | 83 | local num_nodes = get_num_nodes(utils.path_split(utils.path_remove_trailing(new_file_path))) 84 | local is_error = false 85 | for path in utils.path_split(new_file_path) do 86 | idx = idx + 1 87 | local p = utils.path_remove_trailing(path) 88 | if #path_to_create == 0 and vim.fn.has "win32" == 1 then 89 | path_to_create = utils.path_join { p, path_to_create } 90 | else 91 | path_to_create = utils.path_join { path_to_create, p } 92 | end 93 | if is_last_path_file and idx == num_nodes then 94 | create_file(path_to_create) 95 | elseif not utils.file_exists(path_to_create) then 96 | local success = uv.fs_mkdir(path_to_create, 493) 97 | if not success then 98 | utils.notify.error("Could not create folder " .. path_to_create) 99 | is_error = true 100 | break 101 | end 102 | end 103 | end 104 | if not is_error then 105 | utils.notify.info(new_file_path .. " was properly created") 106 | end 107 | events._dispatch_folder_created(new_file_path) 108 | if M.enable_reload then 109 | require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 110 | end 111 | -- INFO: defer needed when reload is automatic (watchers) 112 | vim.defer_fn(function() 113 | utils.focus_file(utils.path_remove_trailing(new_file_path)) 114 | end, 150) 115 | end) 116 | end 117 | 118 | function M.setup(opts) 119 | M.create_in_closed_folder = opts.create_in_closed_folder 120 | M.enable_reload = not opts.filesystem_watchers.enable 121 | end 122 | 123 | return M 124 | -------------------------------------------------------------------------------- /lua/nvim-tree/live-filter.lua: -------------------------------------------------------------------------------- 1 | local a = vim.api 2 | 3 | local view = require "nvim-tree.view" 4 | local Iterator = require "nvim-tree.iterators.node-iterator" 5 | 6 | local M = { 7 | filter = nil, 8 | } 9 | 10 | local function redraw() 11 | require("nvim-tree.renderer").draw() 12 | end 13 | 14 | local function reset_filter(node_) 15 | node_ = node_ or TreeExplorer 16 | Iterator.builder(node_.nodes) 17 | :hidden() 18 | :applier(function(node) 19 | node.hidden = false 20 | end) 21 | :iterate() 22 | end 23 | 24 | local overlay_bufnr = nil 25 | local overlay_winnr = nil 26 | 27 | local function remove_overlay() 28 | if view.View.float.enable then 29 | -- return to normal nvim-tree float behaviour when filter window is closed 30 | a.nvim_create_autocmd("WinLeave", { 31 | pattern = "NvimTree_*", 32 | group = a.nvim_create_augroup("NvimTree", { clear = false }), 33 | callback = view.close, 34 | }) 35 | end 36 | 37 | a.nvim_win_close(overlay_winnr, { force = true }) 38 | overlay_bufnr = nil 39 | overlay_winnr = nil 40 | 41 | if M.filter == "" then 42 | M.clear_filter() 43 | end 44 | end 45 | 46 | local function matches(node) 47 | local path = node.absolute_path 48 | local name = vim.fn.fnamemodify(path, ":t") 49 | return vim.regex(M.filter):match_str(name) ~= nil 50 | end 51 | 52 | function M.apply_filter(node_) 53 | if not M.filter or M.filter == "" then 54 | reset_filter(node_) 55 | return 56 | end 57 | 58 | -- TODO(kiyan): this iterator cannot yet be refactored with the Iterator module 59 | -- since the node mapper is based on its children 60 | local function iterate(node) 61 | local filtered_nodes = 0 62 | local nodes = node.group_next and { node.group_next } or node.nodes 63 | 64 | if nodes then 65 | for _, n in pairs(nodes) do 66 | iterate(n) 67 | if n.hidden then 68 | filtered_nodes = filtered_nodes + 1 69 | end 70 | end 71 | end 72 | 73 | local has_nodes = nodes and (M.always_show_folders or #nodes > filtered_nodes) 74 | node.hidden = not (has_nodes or matches(node)) 75 | end 76 | 77 | iterate(node_ or TreeExplorer) 78 | end 79 | 80 | local function record_char() 81 | vim.schedule(function() 82 | M.filter = a.nvim_buf_get_lines(overlay_bufnr, 0, -1, false)[1] 83 | M.apply_filter() 84 | redraw() 85 | end) 86 | end 87 | 88 | local function configure_buffer_overlay() 89 | overlay_bufnr = a.nvim_create_buf(false, true) 90 | 91 | a.nvim_buf_attach(overlay_bufnr, true, { 92 | on_lines = record_char, 93 | }) 94 | 95 | a.nvim_create_autocmd("InsertLeave", { 96 | callback = remove_overlay, 97 | once = true, 98 | }) 99 | 100 | a.nvim_buf_set_keymap(overlay_bufnr, "i", "", "stopinsert", {}) 101 | end 102 | 103 | local function create_overlay() 104 | local min_width = 20 105 | if view.View.float.enable then 106 | -- don't close nvim-tree float when focus is changed to filter window 107 | a.nvim_clear_autocmds { 108 | event = "WinLeave", 109 | pattern = "NvimTree_*", 110 | group = a.nvim_create_augroup("NvimTree", { clear = false }), 111 | } 112 | 113 | min_width = min_width - 2 114 | end 115 | 116 | configure_buffer_overlay() 117 | overlay_winnr = a.nvim_open_win(overlay_bufnr, true, { 118 | col = 1, 119 | row = 0, 120 | relative = "cursor", 121 | width = math.max(min_width, a.nvim_win_get_width(view.get_winnr()) - #M.prefix - 2), 122 | height = 1, 123 | border = "none", 124 | style = "minimal", 125 | }) 126 | a.nvim_buf_set_option(overlay_bufnr, "modifiable", true) 127 | a.nvim_buf_set_lines(overlay_bufnr, 0, -1, false, { M.filter }) 128 | vim.cmd "startinsert" 129 | a.nvim_win_set_cursor(overlay_winnr, { 1, #M.filter + 1 }) 130 | end 131 | 132 | function M.start_filtering() 133 | M.filter = M.filter or "" 134 | 135 | redraw() 136 | local row = require("nvim-tree.core").get_nodes_starting_line() - 1 137 | local col = #M.prefix > 0 and #M.prefix - 1 or 1 138 | view.set_cursor { row, col } 139 | -- needs scheduling to let the cursor move before initializing the window 140 | vim.schedule(create_overlay) 141 | end 142 | 143 | function M.clear_filter() 144 | M.filter = nil 145 | reset_filter() 146 | redraw() 147 | end 148 | 149 | function M.setup(opts) 150 | M.prefix = opts.live_filter.prefix 151 | M.always_show_folders = opts.live_filter.always_show_folders 152 | end 153 | 154 | return M 155 | -------------------------------------------------------------------------------- /lua/nvim-tree/git/runner.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | local log = require "nvim-tree.log" 3 | local utils = require "nvim-tree.utils" 4 | 5 | local Runner = {} 6 | Runner.__index = Runner 7 | 8 | function Runner:_parse_status_output(line) 9 | local status = line:sub(1, 2) 10 | -- removing `"` when git is returning special file status containing spaces 11 | local path = line:sub(4, -2):gsub('^"', ""):gsub('"$', "") 12 | -- replacing slashes if on windows 13 | if vim.fn.has "win32" == 1 then 14 | path = path:gsub("/", "\\") 15 | end 16 | if #status > 0 and #path > 0 then 17 | self.output[utils.path_remove_trailing(utils.path_join { self.project_root, path })] = status 18 | end 19 | return #line 20 | end 21 | 22 | function Runner:_handle_incoming_data(prev_output, incoming) 23 | if incoming and utils.str_find(incoming, "\n") then 24 | local prev = prev_output .. incoming 25 | local i = 1 26 | for line in prev:gmatch "[^\n]*\n" do 27 | i = i + self:_parse_status_output(line) 28 | end 29 | 30 | return prev:sub(i, -1) 31 | end 32 | 33 | if incoming then 34 | return prev_output .. incoming 35 | end 36 | 37 | for line in prev_output:gmatch "[^\n]*\n" do 38 | self._parse_status_output(line) 39 | end 40 | 41 | return nil 42 | end 43 | 44 | function Runner:_getopts(stdout_handle, stderr_handle) 45 | local untracked = self.list_untracked and "-u" or nil 46 | local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no" 47 | return { 48 | args = { "--no-optional-locks", "status", "--porcelain=v1", ignored, untracked, self.path }, 49 | cwd = self.project_root, 50 | stdio = { nil, stdout_handle, stderr_handle }, 51 | } 52 | end 53 | 54 | function Runner:_log_raw_output(output) 55 | if output and type(output) == "string" then 56 | log.raw("git", "%s", output) 57 | log.line("git", "done") 58 | end 59 | end 60 | 61 | function Runner:_run_git_job() 62 | local handle, pid 63 | local stdout = uv.new_pipe(false) 64 | local stderr = uv.new_pipe(false) 65 | local timer = uv.new_timer() 66 | 67 | local function on_finish(rc) 68 | self.rc = rc or 0 69 | if timer:is_closing() or stdout:is_closing() or stderr:is_closing() or (handle and handle:is_closing()) then 70 | return 71 | end 72 | timer:stop() 73 | timer:close() 74 | stdout:read_stop() 75 | stderr:read_stop() 76 | stdout:close() 77 | stderr:close() 78 | if handle then 79 | handle:close() 80 | end 81 | 82 | pcall(uv.kill, pid) 83 | end 84 | 85 | local opts = self:_getopts(stdout, stderr) 86 | log.line("git", "running job with timeout %dms", self.timeout) 87 | log.line("git", "git %s", table.concat(utils.array_remove_nils(opts.args), " ")) 88 | 89 | handle, pid = uv.spawn( 90 | "git", 91 | opts, 92 | vim.schedule_wrap(function(rc) 93 | on_finish(rc) 94 | end) 95 | ) 96 | 97 | timer:start( 98 | self.timeout, 99 | 0, 100 | vim.schedule_wrap(function() 101 | on_finish(-1) 102 | end) 103 | ) 104 | 105 | local output_leftover = "" 106 | local function manage_stdout(err, data) 107 | if err then 108 | return 109 | end 110 | self:_log_raw_output(data) 111 | output_leftover = self:_handle_incoming_data(output_leftover, data) 112 | end 113 | 114 | local function manage_stderr(_, data) 115 | self:_log_raw_output(data) 116 | end 117 | 118 | uv.read_start(stdout, vim.schedule_wrap(manage_stdout)) 119 | uv.read_start(stderr, vim.schedule_wrap(manage_stderr)) 120 | end 121 | 122 | function Runner:_wait() 123 | local function is_done() 124 | return self.rc ~= nil 125 | end 126 | while not vim.wait(30, is_done) do 127 | end 128 | end 129 | 130 | -- This module runs a git process, which will be killed if it takes more than timeout which defaults to 400ms 131 | function Runner.run(opts) 132 | local ps = log.profile_start("git job %s %s", opts.project_root, opts.path) 133 | 134 | local self = setmetatable({ 135 | project_root = opts.project_root, 136 | path = opts.path, 137 | list_untracked = opts.list_untracked, 138 | list_ignored = opts.list_ignored, 139 | timeout = opts.timeout or 400, 140 | output = {}, 141 | rc = nil, -- -1 indicates timeout 142 | }, Runner) 143 | 144 | self:_run_git_job() 145 | self:_wait() 146 | 147 | log.profile_end(ps, "git job %s %s", opts.project_root, opts.path) 148 | 149 | if self.rc == -1 then 150 | log.line("git", "job timed out %s %s", opts.project_root, opts.path) 151 | elseif self.rc ~= 0 then 152 | log.line("git", "job fail rc %d %s %s", self.rc, opts.project_root, opts.path) 153 | else 154 | log.line("git", "job success %s %s", opts.project_root, opts.path) 155 | end 156 | 157 | return self.output 158 | end 159 | 160 | return Runner 161 | -------------------------------------------------------------------------------- /lua/nvim-tree/lib.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | 3 | local renderer = require "nvim-tree.renderer" 4 | local view = require "nvim-tree.view" 5 | local core = require "nvim-tree.core" 6 | local utils = require "nvim-tree.utils" 7 | 8 | local M = { 9 | target_winid = nil, 10 | } 11 | 12 | function M.get_node_at_cursor() 13 | if not core.get_explorer() then 14 | return 15 | end 16 | 17 | local winnr = view.get_winnr() 18 | if not winnr then 19 | return 20 | end 21 | 22 | local cursor = api.nvim_win_get_cursor(view.get_winnr()) 23 | local line = cursor[1] 24 | if view.is_help_ui() then 25 | local help_lines = require("nvim-tree.renderer.help").compute_lines() 26 | local help_text = utils.get_nodes_by_line(help_lines, 1)[line] 27 | return { name = help_text } 28 | end 29 | 30 | if line == 1 and view.is_root_folder_visible(core.get_cwd()) then 31 | return { name = ".." } 32 | end 33 | 34 | return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[line] 35 | end 36 | 37 | -- If node is grouped, return the last node in the group. Otherwise, return the given node. 38 | function M.get_last_group_node(node) 39 | local next = node 40 | while next.group_next do 41 | next = next.group_next 42 | end 43 | return next 44 | end 45 | 46 | function M.expand_or_collapse(node) 47 | node.open = not node.open 48 | if node.has_children then 49 | node.has_children = false 50 | end 51 | 52 | if #node.nodes == 0 then 53 | core.get_explorer():expand(node) 54 | end 55 | 56 | renderer.draw() 57 | end 58 | 59 | function M.set_target_win() 60 | local id = api.nvim_get_current_win() 61 | local tree_id = view.get_winnr() 62 | if tree_id and id == tree_id then 63 | M.target_winid = 0 64 | return 65 | end 66 | 67 | M.target_winid = id 68 | end 69 | 70 | local function handle_buf_cwd(cwd) 71 | if M.respect_buf_cwd and cwd ~= core.get_cwd() then 72 | require("nvim-tree.actions.root.change-dir").fn(cwd) 73 | end 74 | end 75 | 76 | local function open_view_and_draw() 77 | local cwd = vim.fn.getcwd() 78 | view.open() 79 | handle_buf_cwd(cwd) 80 | renderer.draw() 81 | end 82 | 83 | local function should_hijack_current_buf() 84 | local bufnr = api.nvim_get_current_buf() 85 | local bufname = api.nvim_buf_get_name(bufnr) 86 | local bufmodified = api.nvim_buf_get_option(bufnr, "modified") 87 | local ft = api.nvim_buf_get_option(bufnr, "ft") 88 | 89 | local should_hijack_unnamed = M.hijack_unnamed_buffer_when_opening and bufname == "" and not bufmodified and ft == "" 90 | local should_hijack_dir = bufname ~= "" and vim.fn.isdirectory(bufname) == 1 and M.hijack_directories.enable 91 | 92 | return should_hijack_dir or should_hijack_unnamed 93 | end 94 | 95 | function M.prompt(prompt_input, prompt_select, items_short, items_long, callback) 96 | local function format_item(short) 97 | for i, s in ipairs(items_short) do 98 | if short == s then 99 | return items_long[i] 100 | end 101 | end 102 | return "" 103 | end 104 | 105 | if M.select_prompts then 106 | vim.ui.select(items_short, { prompt = prompt_select, format_item = format_item }, function(item_short) 107 | callback(item_short) 108 | end) 109 | else 110 | vim.ui.input({ prompt = prompt_input }, function(item_short) 111 | callback(item_short) 112 | end) 113 | end 114 | end 115 | 116 | function M.open(cwd) 117 | M.set_target_win() 118 | if not core.get_explorer() or cwd then 119 | core.init(cwd or vim.loop.cwd()) 120 | end 121 | if should_hijack_current_buf() then 122 | view.close() 123 | view.open_in_current_win() 124 | renderer.draw() 125 | else 126 | open_view_and_draw() 127 | end 128 | view.restore_tab_state() 129 | end 130 | 131 | -- @deprecated: use nvim-tree.actions.tree-modifiers.collapse-all.fn 132 | M.collapse_all = require("nvim-tree.actions.tree-modifiers.collapse-all").fn 133 | -- @deprecated: use nvim-tree.actions.root.dir-up.fn 134 | M.dir_up = require("nvim-tree.actions.root.dir-up").fn 135 | -- @deprecated: use nvim-tree.actions.root.change-dir.fn 136 | M.change_dir = require("nvim-tree.actions.root.change-dir").fn 137 | -- @deprecated: use nvim-tree.actions.reloaders.reloaders.reload_explorer 138 | M.refresh_tree = require("nvim-tree.actions.reloaders.reloaders").reload_explorer 139 | -- @deprecated: use nvim-tree.actions.reloaders.reloaders.reload_git 140 | M.reload_git = require("nvim-tree.actions.reloaders.reloaders").reload_git 141 | -- @deprecated: use nvim-tree.actions.finders.find-file.fn 142 | M.set_index_and_redraw = require("nvim-tree.actions.finders.find-file").fn 143 | 144 | function M.setup(opts) 145 | M.hijack_unnamed_buffer_when_opening = opts.hijack_unnamed_buffer_when_opening 146 | M.hijack_directories = opts.hijack_directories 147 | M.respect_buf_cwd = opts.respect_buf_cwd 148 | M.select_prompts = opts.select_prompts 149 | end 150 | 151 | return M 152 | -------------------------------------------------------------------------------- /lua/nvim-tree/git/init.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local log = require "nvim-tree.log" 4 | local utils = require "nvim-tree.utils" 5 | local git_utils = require "nvim-tree.git.utils" 6 | local Runner = require "nvim-tree.git.runner" 7 | local Watcher = require("nvim-tree.watcher").Watcher 8 | local Iterator = require "nvim-tree.iterators.node-iterator" 9 | 10 | local M = { 11 | config = {}, 12 | projects = {}, 13 | cwd_to_project_root = {}, 14 | } 15 | 16 | function M.reload() 17 | if not M.config.git.enable then 18 | return {} 19 | end 20 | 21 | for project_root in pairs(M.projects) do 22 | M.reload_project(project_root) 23 | end 24 | 25 | return M.projects 26 | end 27 | 28 | function M.reload_project(project_root, path) 29 | local project = M.projects[project_root] 30 | if not project or not M.config.git.enable then 31 | return 32 | end 33 | 34 | if path and path:find(project_root, 1, true) ~= 1 then 35 | return 36 | end 37 | 38 | local git_status = Runner.run { 39 | project_root = project_root, 40 | path = path, 41 | list_untracked = git_utils.should_show_untracked(project_root), 42 | list_ignored = true, 43 | timeout = M.config.git.timeout, 44 | } 45 | 46 | if path then 47 | for p in pairs(project.files) do 48 | if p:find(path, 1, true) == 1 then 49 | project.files[p] = nil 50 | end 51 | end 52 | project.files = vim.tbl_deep_extend("force", project.files, git_status) 53 | else 54 | project.files = git_status 55 | end 56 | 57 | project.dirs = git_utils.file_status_to_dir_status(project.files, project_root) 58 | end 59 | 60 | function M.get_project(project_root) 61 | return M.projects[project_root] 62 | end 63 | 64 | function M.get_project_root(cwd) 65 | if not M.config.git.enable then 66 | return nil 67 | end 68 | 69 | if M.cwd_to_project_root[cwd] then 70 | return M.cwd_to_project_root[cwd] 71 | end 72 | 73 | if M.cwd_to_project_root[cwd] == false then 74 | return nil 75 | end 76 | 77 | local stat, _ = uv.fs_stat(cwd) 78 | if not stat or stat.type ~= "directory" then 79 | return nil 80 | end 81 | 82 | M.cwd_to_project_root[cwd] = git_utils.get_toplevel(cwd) 83 | return M.cwd_to_project_root[cwd] 84 | end 85 | 86 | local function reload_tree_at(project_root) 87 | if not M.config.git.enable then 88 | return nil 89 | end 90 | 91 | log.line("watcher", "git event executing '%s'", project_root) 92 | local root_node = utils.get_node_from_path(project_root) 93 | if not root_node then 94 | return 95 | end 96 | 97 | M.reload_project(project_root) 98 | local project = M.get_project(project_root) 99 | 100 | local project_files = project.files and project.files or {} 101 | local project_dirs = project.dirs and project.dirs or {} 102 | 103 | Iterator.builder(root_node.nodes) 104 | :hidden() 105 | :applier(function(node) 106 | local parent_ignored = node.parent.git_status == "!!" 107 | node.git_status = project_dirs[node.absolute_path] or project_files[node.absolute_path] 108 | if not node.git_status and parent_ignored then 109 | node.git_status = "!!" 110 | end 111 | end) 112 | :recursor(function(node) 113 | return node.nodes and #node.nodes > 0 and node.nodes 114 | end) 115 | :iterate() 116 | 117 | require("nvim-tree.renderer").draw() 118 | end 119 | 120 | function M.load_project_status(cwd) 121 | if not M.config.git.enable then 122 | return {} 123 | end 124 | 125 | local project_root = M.get_project_root(cwd) 126 | if not project_root then 127 | M.cwd_to_project_root[cwd] = false 128 | return {} 129 | end 130 | 131 | local status = M.projects[project_root] 132 | if status then 133 | return status 134 | end 135 | 136 | local git_status = Runner.run { 137 | project_root = project_root, 138 | list_untracked = git_utils.should_show_untracked(project_root), 139 | list_ignored = true, 140 | timeout = M.config.git.timeout, 141 | } 142 | 143 | local watcher = nil 144 | if M.config.filesystem_watchers.enable then 145 | log.line("watcher", "git start") 146 | 147 | local callback = function(w) 148 | log.line("watcher", "git event scheduled '%s'", w.project_root) 149 | utils.debounce("git:watcher:" .. w.project_root, M.config.filesystem_watchers.debounce_delay, function() 150 | reload_tree_at(w.project_root) 151 | end) 152 | end 153 | 154 | watcher = Watcher:new(utils.path_join { project_root, ".git" }, callback, { 155 | project_root = project_root, 156 | }) 157 | end 158 | 159 | M.projects[project_root] = { 160 | files = git_status, 161 | dirs = git_utils.file_status_to_dir_status(git_status, project_root), 162 | watcher = watcher, 163 | } 164 | return M.projects[project_root] 165 | end 166 | 167 | function M.purge_state() 168 | M.projects = {} 169 | M.cwd_to_project_root = {} 170 | end 171 | 172 | function M.setup(opts) 173 | M.config.git = opts.git 174 | M.config.filesystem_watchers = opts.filesystem_watchers 175 | end 176 | 177 | return M 178 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/dispatch.lua: -------------------------------------------------------------------------------- 1 | local view = require "nvim-tree.view" 2 | local lib = require "nvim-tree.lib" 3 | 4 | local M = {} 5 | 6 | local Actions = { 7 | close = view.close, 8 | 9 | -- Tree modifiers 10 | collapse_all = require("nvim-tree.actions.tree-modifiers.collapse-all").fn, 11 | expand_all = require("nvim-tree.actions.tree-modifiers.expand-all").fn, 12 | toggle_dotfiles = require("nvim-tree.actions.tree-modifiers.toggles").dotfiles, 13 | toggle_custom = require("nvim-tree.actions.tree-modifiers.toggles").custom, 14 | toggle_git_ignored = require("nvim-tree.actions.tree-modifiers.toggles").git_ignored, 15 | 16 | -- Filesystem operations 17 | copy_absolute_path = require("nvim-tree.actions.fs.copy-paste").copy_absolute_path, 18 | copy_name = require("nvim-tree.actions.fs.copy-paste").copy_filename, 19 | copy_path = require("nvim-tree.actions.fs.copy-paste").copy_path, 20 | copy = require("nvim-tree.actions.fs.copy-paste").copy, 21 | create = require("nvim-tree.actions.fs.create-file").fn, 22 | cut = require("nvim-tree.actions.fs.copy-paste").cut, 23 | full_rename = require("nvim-tree.actions.fs.rename-file").fn(true), 24 | paste = require("nvim-tree.actions.fs.copy-paste").paste, 25 | trash = require("nvim-tree.actions.fs.trash").fn, 26 | remove = require("nvim-tree.actions.fs.remove-file").fn, 27 | rename = require("nvim-tree.actions.fs.rename-file").fn(false), 28 | 29 | -- Movements in tree 30 | close_node = require("nvim-tree.actions.moves.parent").fn(true), 31 | first_sibling = require("nvim-tree.actions.moves.sibling").fn "first", 32 | last_sibling = require("nvim-tree.actions.moves.sibling").fn "last", 33 | next_diag_item = require("nvim-tree.actions.moves.item").fn("next", "diag"), 34 | next_git_item = require("nvim-tree.actions.moves.item").fn("next", "git"), 35 | next_sibling = require("nvim-tree.actions.moves.sibling").fn "next", 36 | parent_node = require("nvim-tree.actions.moves.parent").fn(false), 37 | prev_diag_item = require("nvim-tree.actions.moves.item").fn("prev", "diag"), 38 | prev_git_item = require("nvim-tree.actions.moves.item").fn("prev", "git"), 39 | prev_sibling = require("nvim-tree.actions.moves.sibling").fn "prev", 40 | 41 | -- Other types 42 | refresh = require("nvim-tree.actions.reloaders.reloaders").reload_explorer, 43 | dir_up = require("nvim-tree.actions.root.dir-up").fn, 44 | search_node = require("nvim-tree.actions.finders.search-node").fn, 45 | run_file_command = require("nvim-tree.actions.node.run-command").run_file_command, 46 | toggle_file_info = require("nvim-tree.actions.node.file-popup").toggle_file_info, 47 | system_open = require("nvim-tree.actions.node.system-open").fn, 48 | toggle_mark = require("nvim-tree.marks").toggle_mark, 49 | bulk_move = require("nvim-tree.marks.bulk-move").bulk_move, 50 | } 51 | 52 | local function handle_action_on_help_ui(action) 53 | if action == "close" or action == "toggle_help" then 54 | require("nvim-tree.actions.tree-modifiers.toggles").help() 55 | end 56 | end 57 | 58 | local function handle_filter_actions(action) 59 | if action == "live_filter" then 60 | require("nvim-tree.live-filter").start_filtering() 61 | elseif action == "clear_live_filter" then 62 | require("nvim-tree.live-filter").clear_filter() 63 | end 64 | end 65 | 66 | local function change_dir_action(node) 67 | if node.name == ".." then 68 | require("nvim-tree.actions.root.change-dir").fn ".." 69 | elseif node.nodes ~= nil then 70 | require("nvim-tree.actions.root.change-dir").fn(lib.get_last_group_node(node).absolute_path) 71 | end 72 | end 73 | 74 | local function open_file(action, node) 75 | local path = node.absolute_path 76 | if node.link_to and not node.nodes then 77 | path = node.link_to 78 | end 79 | require("nvim-tree.actions.node.open-file").fn(action, path) 80 | end 81 | 82 | local function handle_tree_actions(action) 83 | local node = lib.get_node_at_cursor() 84 | if not node then 85 | return 86 | end 87 | 88 | local custom_function = M.custom_keypress_funcs[action] 89 | local defined_action = Actions[action] 90 | 91 | if type(custom_function) == "function" then 92 | return custom_function(node) 93 | elseif defined_action then 94 | return defined_action(node) 95 | end 96 | 97 | local is_parent = node.name == ".." 98 | 99 | if action == "preview" and is_parent then 100 | return 101 | end 102 | 103 | if action == "cd" or is_parent then 104 | return change_dir_action(node) 105 | end 106 | 107 | if node.nodes then 108 | lib.expand_or_collapse(node) 109 | else 110 | open_file(action, node) 111 | end 112 | end 113 | 114 | function M.dispatch(action) 115 | if view.is_help_ui() or action == "toggle_help" then 116 | handle_action_on_help_ui(action) 117 | elseif action == "live_filter" or action == "clear_live_filter" then 118 | handle_filter_actions(action) 119 | else 120 | handle_tree_actions(action) 121 | end 122 | end 123 | 124 | function M.setup(custom_keypress_funcs) 125 | M.custom_keypress_funcs = custom_keypress_funcs 126 | end 127 | 128 | return M 129 | -------------------------------------------------------------------------------- /lua/nvim-tree/diagnostics.lua: -------------------------------------------------------------------------------- 1 | local a = vim.api 2 | local utils = require "nvim-tree.utils" 3 | local view = require "nvim-tree.view" 4 | local core = require "nvim-tree.core" 5 | local log = require "nvim-tree.log" 6 | 7 | local M = {} 8 | 9 | local GROUP = "NvimTreeDiagnosticSigns" 10 | 11 | local severity_levels = { Error = 1, Warning = 2, Information = 3, Hint = 4 } 12 | local sign_names = { 13 | { "NvimTreeSignError", "NvimTreeLspDiagnosticsError" }, 14 | { "NvimTreeSignWarning", "NvimTreeLspDiagnosticsWarning" }, 15 | { "NvimTreeSignInformation", "NvimTreeLspDiagnosticsInformation" }, 16 | { "NvimTreeSignHint", "NvimTreeLspDiagnosticsHint" }, 17 | } 18 | 19 | local function add_sign(linenr, severity) 20 | local buf = view.get_bufnr() 21 | if not a.nvim_buf_is_valid(buf) or not a.nvim_buf_is_loaded(buf) then 22 | return 23 | end 24 | local sign_name = sign_names[severity][1] 25 | vim.fn.sign_place(0, GROUP, sign_name, buf, { lnum = linenr, priority = 2 }) 26 | end 27 | 28 | local function from_nvim_lsp() 29 | local buffer_severity = {} 30 | 31 | for _, diagnostic in ipairs(vim.diagnostic.get()) do 32 | local buf = diagnostic.bufnr 33 | if a.nvim_buf_is_valid(buf) then 34 | local bufname = a.nvim_buf_get_name(buf) 35 | local lowest_severity = buffer_severity[bufname] 36 | if not lowest_severity or diagnostic.severity < lowest_severity then 37 | buffer_severity[bufname] = diagnostic.severity 38 | end 39 | end 40 | end 41 | 42 | return buffer_severity 43 | end 44 | 45 | local function from_coc() 46 | if vim.g.coc_service_initialized ~= 1 then 47 | return {} 48 | end 49 | 50 | local diagnostic_list = vim.fn.CocAction "diagnosticList" 51 | if type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then 52 | return {} 53 | end 54 | 55 | local buffer_severity = {} 56 | local diagnostics = {} 57 | 58 | for _, diagnostic in ipairs(diagnostic_list) do 59 | local bufname = diagnostic.file 60 | local severity = severity_levels[diagnostic.severity] 61 | 62 | local severity_list = diagnostics[bufname] or {} 63 | table.insert(severity_list, severity) 64 | diagnostics[bufname] = severity_list 65 | end 66 | 67 | for bufname, severity_list in pairs(diagnostics) do 68 | if not buffer_severity[bufname] then 69 | local severity = math.min(unpack(severity_list)) 70 | buffer_severity[bufname] = severity 71 | end 72 | end 73 | 74 | return buffer_severity 75 | end 76 | 77 | local function is_using_coc() 78 | return vim.g.coc_service_initialized == 1 79 | end 80 | 81 | function M.clear() 82 | if not M.enable or not view.is_buf_valid(view.get_bufnr()) then 83 | return 84 | end 85 | 86 | vim.fn.sign_unplace(GROUP) 87 | end 88 | 89 | function M.update() 90 | if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then 91 | return 92 | end 93 | utils.debounce("diagnostics", M.debounce_delay, function() 94 | local ps = log.profile_start "diagnostics update" 95 | log.line("diagnostics", "update") 96 | 97 | local buffer_severity 98 | if is_using_coc() then 99 | buffer_severity = from_coc() 100 | else 101 | buffer_severity = from_nvim_lsp() 102 | end 103 | 104 | M.clear() 105 | 106 | local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line()) 107 | for _, node in pairs(nodes_by_line) do 108 | node.diag_status = nil 109 | end 110 | 111 | for bufname, severity in pairs(buffer_severity) do 112 | local bufpath = utils.canonical_path(bufname) 113 | log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity) 114 | if 0 < severity and severity < 5 then 115 | for line, node in pairs(nodes_by_line) do 116 | local nodepath = utils.canonical_path(node.absolute_path) 117 | log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) 118 | if M.show_on_dirs and vim.startswith(bufpath, nodepath) then 119 | log.line("diagnostics", " matched fold node '%s'", node.absolute_path) 120 | node.diag_status = severity 121 | add_sign(line, severity) 122 | elseif nodepath == bufpath then 123 | log.line("diagnostics", " matched file node '%s'", node.absolute_path) 124 | node.diag_status = severity 125 | add_sign(line, severity) 126 | end 127 | end 128 | end 129 | end 130 | log.profile_end(ps, "diagnostics update") 131 | end) 132 | end 133 | 134 | local links = { 135 | NvimTreeLspDiagnosticsError = "DiagnosticError", 136 | NvimTreeLspDiagnosticsWarning = "DiagnosticWarn", 137 | NvimTreeLspDiagnosticsInformation = "DiagnosticInfo", 138 | NvimTreeLspDiagnosticsHint = "DiagnosticHint", 139 | } 140 | 141 | function M.setup(opts) 142 | M.enable = opts.diagnostics.enable 143 | M.debounce_delay = opts.diagnostics.debounce_delay 144 | 145 | if M.enable then 146 | log.line("diagnostics", "setup") 147 | end 148 | 149 | M.show_on_dirs = opts.diagnostics.show_on_dirs 150 | vim.fn.sign_define(sign_names[1][1], { text = opts.diagnostics.icons.error, texthl = sign_names[1][2] }) 151 | vim.fn.sign_define(sign_names[2][1], { text = opts.diagnostics.icons.warning, texthl = sign_names[2][2] }) 152 | vim.fn.sign_define(sign_names[3][1], { text = opts.diagnostics.icons.info, texthl = sign_names[3][2] }) 153 | vim.fn.sign_define(sign_names[4][1], { text = opts.diagnostics.icons.hint, texthl = sign_names[4][2] }) 154 | 155 | for lhs, rhs in pairs(links) do 156 | vim.cmd("hi def link " .. lhs .. " " .. rhs) 157 | end 158 | end 159 | 160 | return M 161 | -------------------------------------------------------------------------------- /lua/nvim-tree/explorer/sorters.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | sort_by = nil, 3 | node_comparator = nil, 4 | } 5 | 6 | ---Create a shallow copy of a portion of a list. 7 | ---@param t table 8 | ---@param first integer First index, inclusive 9 | ---@param last integer Last index, inclusive 10 | ---@return table 11 | local function tbl_slice(t, first, last) 12 | local slice = {} 13 | for i = first, last or #t, 1 do 14 | table.insert(slice, t[i]) 15 | end 16 | 17 | return slice 18 | end 19 | 20 | local function merge(t, first, mid, last, comparator) 21 | local n1 = mid - first + 1 22 | local n2 = last - mid 23 | local ls = tbl_slice(t, first, mid) 24 | local rs = tbl_slice(t, mid + 1, last) 25 | local i = 1 26 | local j = 1 27 | local k = first 28 | 29 | while i <= n1 and j <= n2 do 30 | if comparator(ls[i], rs[j]) then 31 | t[k] = ls[i] 32 | i = i + 1 33 | else 34 | t[k] = rs[j] 35 | j = j + 1 36 | end 37 | k = k + 1 38 | end 39 | 40 | while i <= n1 do 41 | t[k] = ls[i] 42 | i = i + 1 43 | k = k + 1 44 | end 45 | 46 | while j <= n2 do 47 | t[k] = rs[j] 48 | j = j + 1 49 | k = k + 1 50 | end 51 | end 52 | 53 | local function split_merge(t, first, last, comparator) 54 | if (last - first) < 1 then 55 | return 56 | end 57 | 58 | local mid = math.floor((first + last) / 2) 59 | 60 | split_merge(t, first, mid, comparator) 61 | split_merge(t, mid + 1, last, comparator) 62 | merge(t, first, mid, last, comparator) 63 | end 64 | 65 | ---Perform a merge sort on a given list. 66 | ---@param t any[] 67 | ---@param comparator function|nil 68 | function M.merge_sort(t, comparator) 69 | if type(M.sort_by) == "function" then 70 | local t_user = {} 71 | local origin_index = {} 72 | 73 | for _, n in ipairs(t) do 74 | table.insert(t_user, { 75 | absolute_path = n.absolute_path, 76 | executable = n.executable, 77 | extension = n.extension, 78 | link_to = n.link_to, 79 | name = n.name, 80 | type = n.type, 81 | }) 82 | table.insert(origin_index, n) 83 | end 84 | 85 | M.sort_by(t_user) 86 | 87 | -- do merge sort for prevent memory exceed 88 | local user_index = {} 89 | for i, v in ipairs(t_user) do 90 | if type(v.absolute_path) == "string" and user_index[v.absolute_path] == nil then 91 | user_index[v.absolute_path] = i 92 | end 93 | end 94 | 95 | -- if missing value found, then using origin_index 96 | local mini_comparator = function(a, b) 97 | local a_index = user_index[a.absolute_path] or origin_index[a.absolute_path] 98 | local b_index = user_index[b.absolute_path] or origin_index[b.absolute_path] 99 | 100 | if type(a_index) == "number" and type(b_index) == "number" then 101 | return a_index <= b_index 102 | end 103 | return (a_index or 0) <= (b_index or 0) 104 | end 105 | 106 | split_merge(t, 1, #t, mini_comparator) -- sort by user order 107 | else 108 | if not comparator then 109 | comparator = function(left, right) 110 | return left < right 111 | end 112 | end 113 | split_merge(t, 1, #t, comparator) 114 | end 115 | end 116 | 117 | local function node_comparator_name_ignorecase_or_not(a, b, ignorecase) 118 | if not (a and b) then 119 | return true 120 | end 121 | if a.nodes and not b.nodes then 122 | return true 123 | elseif not a.nodes and b.nodes then 124 | return false 125 | end 126 | 127 | if ignorecase then 128 | return a.name:lower() <= b.name:lower() 129 | else 130 | return a.name <= b.name 131 | end 132 | end 133 | 134 | function M.node_comparator_name_case_sensisive(a, b) 135 | return node_comparator_name_ignorecase_or_not(a, b, false) 136 | end 137 | 138 | function M.node_comparator_name_ignorecase(a, b) 139 | return node_comparator_name_ignorecase_or_not(a, b, true) 140 | end 141 | 142 | function M.node_comparator_modification_time(a, b) 143 | if not (a and b) then 144 | return true 145 | end 146 | if a.nodes and not b.nodes then 147 | return true 148 | elseif not a.nodes and b.nodes then 149 | return false 150 | end 151 | 152 | local last_modified_a = 0 153 | local last_modified_b = 0 154 | 155 | if a.fs_stat ~= nil then 156 | last_modified_a = a.fs_stat.mtime.sec 157 | end 158 | 159 | if b.fs_stat ~= nil then 160 | last_modified_b = b.fs_stat.mtime.sec 161 | end 162 | 163 | return last_modified_b <= last_modified_a 164 | end 165 | 166 | function M.node_comparator_extension(a, b) 167 | if not (a and b) then 168 | return true 169 | end 170 | 171 | if a.nodes and not b.nodes then 172 | return true 173 | elseif not a.nodes and b.nodes then 174 | return false 175 | end 176 | 177 | if not (a.extension and b.extension) then 178 | return true 179 | end 180 | 181 | if a.extension and not b.extension then 182 | return true 183 | elseif not a.extension and b.extension then 184 | return false 185 | end 186 | 187 | return a.extension:lower() <= b.extension:lower() 188 | end 189 | 190 | function M.setup(opts) 191 | M.sort_by = opts.sort_by 192 | if M.sort_by and type(M.sort_by) == "function" then 193 | M.node_comparator = M.sort_by 194 | elseif M.sort_by == "modification_time" then 195 | M.node_comparator = M.node_comparator_modification_time 196 | elseif M.sort_by == "case_sensitive" then 197 | M.node_comparator = M.node_comparator_name_case_sensisive 198 | elseif M.sort_by == "extension" then 199 | M.node_comparator = M.node_comparator_extension 200 | else 201 | M.node_comparator = M.node_comparator_name_ignorecase 202 | end 203 | end 204 | 205 | return M 206 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/components/git.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | 3 | local M = { 4 | SIGN_GROUP = "NvimTreeGitSigns", 5 | } 6 | 7 | local function build_icons_table(i) 8 | return { 9 | ["M "] = { { icon = i.staged, hl = "NvimTreeGitStaged" } }, 10 | [" M"] = { { icon = i.unstaged, hl = "NvimTreeGitDirty" } }, 11 | ["C "] = { { icon = i.staged, hl = "NvimTreeGitStaged" } }, 12 | [" C"] = { { icon = i.unstaged, hl = "NvimTreeGitDirty" } }, 13 | ["CM"] = { { icon = i.unstaged, hl = "NvimTreeGitDirty" } }, 14 | [" T"] = { { icon = i.unstaged, hl = "NvimTreeGitDirty" } }, 15 | ["T "] = { { icon = i.staged, hl = "NvimTreeGitStaged" } }, 16 | ["MM"] = { 17 | { icon = i.staged, hl = "NvimTreeGitStaged" }, 18 | { icon = i.unstaged, hl = "NvimTreeGitDirty" }, 19 | }, 20 | ["MD"] = { 21 | { icon = i.staged, hl = "NvimTreeGitStaged" }, 22 | }, 23 | ["A "] = { 24 | { icon = i.staged, hl = "NvimTreeGitStaged" }, 25 | }, 26 | ["AD"] = { 27 | { icon = i.staged, hl = "NvimTreeGitStaged" }, 28 | }, 29 | [" A"] = { 30 | { icon = i.untracked, hl = "NvimTreeGitNew" }, 31 | }, 32 | -- not sure about this one 33 | ["AA"] = { 34 | { icon = i.unmerged, hl = "NvimTreeGitMerge" }, 35 | { icon = i.untracked, hl = "NvimTreeGitNew" }, 36 | }, 37 | ["AU"] = { 38 | { icon = i.unmerged, hl = "NvimTreeGitMerge" }, 39 | { icon = i.untracked, hl = "NvimTreeGitNew" }, 40 | }, 41 | ["AM"] = { 42 | { icon = i.staged, hl = "NvimTreeGitStaged" }, 43 | { icon = i.unstaged, hl = "NvimTreeGitDirty" }, 44 | }, 45 | ["??"] = { { icon = i.untracked, hl = "NvimTreeGitNew" } }, 46 | ["R "] = { { icon = i.renamed, hl = "NvimTreeGitRenamed" } }, 47 | [" R"] = { { icon = i.renamed, hl = "NvimTreeGitRenamed" } }, 48 | ["RM"] = { 49 | { icon = i.unstaged, hl = "NvimTreeGitDirty" }, 50 | { icon = i.renamed, hl = "NvimTreeGitRenamed" }, 51 | }, 52 | ["UU"] = { { icon = i.unmerged, hl = "NvimTreeGitMerge" } }, 53 | ["UD"] = { { icon = i.unmerged, hl = "NvimTreeGitMerge" } }, 54 | ["UA"] = { { icon = i.unmerged, hl = "NvimTreeGitMerge" } }, 55 | [" D"] = { { icon = i.deleted, hl = "NvimTreeGitDeleted" } }, 56 | ["D "] = { { icon = i.deleted, hl = "NvimTreeGitDeleted" } }, 57 | ["RD"] = { { icon = i.deleted, hl = "NvimTreeGitDeleted" } }, 58 | ["DD"] = { { icon = i.deleted, hl = "NvimTreeGitDeleted" } }, 59 | ["DU"] = { 60 | { icon = i.deleted, hl = "NvimTreeGitDeleted" }, 61 | { icon = i.unmerged, hl = "NvimTreeGitMerge" }, 62 | }, 63 | ["!!"] = { { icon = i.ignored, hl = "NvimTreeGitIgnored" } }, 64 | dirty = { { icon = i.unstaged, hl = "NvimTreeGitDirty" } }, 65 | } 66 | end 67 | 68 | local function nil_() end 69 | 70 | local function warn_status(git_status) 71 | utils.notify.warn( 72 | 'Unrecognized git state "' 73 | .. git_status 74 | .. '". Please open up an issue on https://github.com/kyazdani42/nvim-tree.lua/issues with this message.' 75 | ) 76 | end 77 | 78 | local function get_icons_(node) 79 | local git_status = node.git_status 80 | if not git_status then 81 | return nil 82 | end 83 | 84 | local icons = M.git_icons[git_status] 85 | if not icons then 86 | if not M.config.highlight_git then 87 | warn_status(git_status) 88 | end 89 | return nil 90 | end 91 | 92 | return icons 93 | end 94 | 95 | local git_hl = { 96 | ["M "] = "NvimTreeFileStaged", 97 | ["C "] = "NvimTreeFileStaged", 98 | ["AA"] = "NvimTreeFileStaged", 99 | ["AD"] = "NvimTreeFileStaged", 100 | ["MD"] = "NvimTreeFileStaged", 101 | ["T "] = "NvimTreeFileStaged", 102 | ["TT"] = "NvimTreeFileStaged", 103 | [" M"] = "NvimTreeFileDirty", 104 | ["CM"] = "NvimTreeFileDirty", 105 | [" C"] = "NvimTreeFileDirty", 106 | [" T"] = "NvimTreeFileDirty", 107 | ["MM"] = "NvimTreeFileDirty", 108 | ["AM"] = "NvimTreeFileDirty", 109 | dirty = "NvimTreeFileDirty", 110 | ["A "] = "NvimTreeFileNew", 111 | ["??"] = "NvimTreeFileNew", 112 | ["AU"] = "NvimTreeFileMerge", 113 | ["UU"] = "NvimTreeFileMerge", 114 | ["UD"] = "NvimTreeFileMerge", 115 | ["DU"] = "NvimTreeFileMerge", 116 | ["UA"] = "NvimTreeFileMerge", 117 | [" D"] = "NvimTreeFileDeleted", 118 | ["DD"] = "NvimTreeFileDeleted", 119 | ["RD"] = "NvimTreeFileDeleted", 120 | ["D "] = "NvimTreeFileDeleted", 121 | ["R "] = "NvimTreeFileRenamed", 122 | ["RM"] = "NvimTreeFileRenamed", 123 | [" R"] = "NvimTreeFileRenamed", 124 | ["!!"] = "NvimTreeFileIgnored", 125 | [" A"] = "none", 126 | } 127 | 128 | function M.setup_signs(i) 129 | vim.fn.sign_define("NvimTreeGitDirty", { text = i.unstaged, texthl = "NvimTreeGitDirty" }) 130 | vim.fn.sign_define("NvimTreeGitStaged", { text = i.staged, texthl = "NvimTreeGitStaged" }) 131 | vim.fn.sign_define("NvimTreeGitMerge", { text = i.unmerged, texthl = "NvimTreeGitMerge" }) 132 | vim.fn.sign_define("NvimTreeGitRenamed", { text = i.renamed, texthl = "NvimTreeGitRenamed" }) 133 | vim.fn.sign_define("NvimTreeGitNew", { text = i.untracked, texthl = "NvimTreeGitNew" }) 134 | vim.fn.sign_define("NvimTreeGitDeleted", { text = i.deleted, texthl = "NvimTreeGitDeleted" }) 135 | vim.fn.sign_define("NvimTreeGitIgnored", { text = i.ignored, texthl = "NvimTreeGitIgnored" }) 136 | end 137 | 138 | local function get_highlight_(node) 139 | local git_status = node.git_status 140 | if not git_status then 141 | return 142 | end 143 | 144 | return git_hl[git_status] 145 | end 146 | 147 | function M.setup(opts) 148 | M.config = opts.renderer 149 | 150 | M.git_icons = build_icons_table(opts.renderer.icons.glyphs.git) 151 | 152 | M.setup_signs(opts.renderer.icons.glyphs.git) 153 | 154 | if opts.renderer.icons.show.git then 155 | M.get_icons = get_icons_ 156 | else 157 | M.get_icons = nil_ 158 | end 159 | 160 | if opts.renderer.highlight_git then 161 | M.get_highlight = get_highlight_ 162 | else 163 | M.get_highlight = nil_ 164 | end 165 | end 166 | 167 | return M 168 | -------------------------------------------------------------------------------- /lua/nvim-tree/api.lua: -------------------------------------------------------------------------------- 1 | local Api = { 2 | tree = {}, 3 | node = { navigate = { sibling = {}, git = {}, diagnostics = {} }, run = {}, open = {} }, 4 | events = {}, 5 | marks = { bulk = {}, navigate = {} }, 6 | fs = { copy = {} }, 7 | git = {}, 8 | live_filter = {}, 9 | } 10 | 11 | local function inject_node(f) 12 | return function(node) 13 | node = node or require("nvim-tree.lib").get_node_at_cursor() 14 | f(node) 15 | end 16 | end 17 | 18 | Api.tree.open = require("nvim-tree").open 19 | Api.tree.toggle = require("nvim-tree").toggle 20 | Api.tree.close = require("nvim-tree.view").close 21 | Api.tree.focus = require("nvim-tree").focus 22 | Api.tree.reload = require("nvim-tree.actions.reloaders.reloaders").reload_explorer 23 | Api.tree.change_root = require("nvim-tree").change_dir 24 | Api.tree.change_root_to_node = inject_node(function(node) 25 | if node.name == ".." then 26 | require("nvim-tree.actions.root.change-dir").fn ".." 27 | elseif node.nodes ~= nil then 28 | require("nvim-tree.actions.root.change-dir").fn(require("nvim-tree.lib").get_last_group_node(node).absolute_path) 29 | end 30 | end) 31 | Api.tree.change_root_to_parent = inject_node(require("nvim-tree.actions.root.dir-up").fn) 32 | Api.tree.get_node_under_cursor = require("nvim-tree.lib").get_node_at_cursor 33 | Api.tree.find_file = require("nvim-tree.actions.finders.find-file").fn 34 | Api.tree.search_node = require("nvim-tree.actions.finders.search-node").fn 35 | Api.tree.collapse_all = require("nvim-tree.actions.tree-modifiers.collapse-all").fn 36 | Api.tree.expand_all = inject_node(require("nvim-tree.actions.tree-modifiers.expand-all").fn) 37 | Api.tree.toggle_gitignore_filter = require("nvim-tree.actions.tree-modifiers.toggles").git_ignored 38 | Api.tree.toggle_custom_filter = require("nvim-tree.actions.tree-modifiers.toggles").custom 39 | Api.tree.toggle_hidden_filter = require("nvim-tree.actions.tree-modifiers.toggles").dotfiles 40 | Api.tree.toggle_help = require("nvim-tree.actions.tree-modifiers.toggles").help 41 | 42 | Api.fs.create = inject_node(require("nvim-tree.actions.fs.create-file").fn) 43 | Api.fs.remove = inject_node(require("nvim-tree.actions.fs.remove-file").fn) 44 | Api.fs.trash = inject_node(require("nvim-tree.actions.fs.trash").fn) 45 | Api.fs.rename = inject_node(require("nvim-tree.actions.fs.rename-file").fn(false)) 46 | Api.fs.rename_sub = inject_node(require("nvim-tree.actions.fs.rename-file").fn(true)) 47 | Api.fs.cut = inject_node(require("nvim-tree.actions.fs.copy-paste").cut) 48 | Api.fs.paste = inject_node(require("nvim-tree.actions.fs.copy-paste").paste) 49 | Api.fs.print_clipboard = require("nvim-tree.actions.fs.copy-paste").print_clipboard 50 | Api.fs.copy.node = inject_node(require("nvim-tree.actions.fs.copy-paste").copy) 51 | Api.fs.copy.absolute_path = inject_node(require("nvim-tree.actions.fs.copy-paste").copy_absolute_path) 52 | Api.fs.copy.filename = inject_node(require("nvim-tree.actions.fs.copy-paste").copy_filename) 53 | Api.fs.copy.relative_path = inject_node(require("nvim-tree.actions.fs.copy-paste").copy_path) 54 | 55 | local function edit(mode, node) 56 | local path = node.absolute_path 57 | if node.link_to and not node.nodes then 58 | path = node.link_to 59 | end 60 | require("nvim-tree.actions.node.open-file").fn(mode, path) 61 | end 62 | 63 | local function open_or_expand_or_dir_up(mode) 64 | return function(node) 65 | if node.name == ".." then 66 | require("nvim-tree.actions.root.change-dir").fn ".." 67 | elseif node.nodes then 68 | require("nvim-tree.lib").expand_or_collapse(node) 69 | else 70 | edit(mode, node) 71 | end 72 | end 73 | end 74 | 75 | local function open_preview(node) 76 | if node.nodes or node.name == ".." then 77 | return 78 | end 79 | 80 | edit("preview", node) 81 | end 82 | 83 | Api.node.open.edit = inject_node(open_or_expand_or_dir_up "edit") 84 | Api.node.open.replace_tree_buffer = inject_node(open_or_expand_or_dir_up "edit_in_place") 85 | Api.node.open.no_window_picker = inject_node(open_or_expand_or_dir_up "edit_no_picker") 86 | Api.node.open.vertical = inject_node(open_or_expand_or_dir_up "vsplit") 87 | Api.node.open.horizontal = inject_node(open_or_expand_or_dir_up "split") 88 | Api.node.open.tab = inject_node(open_or_expand_or_dir_up "tabnew") 89 | Api.node.open.preview = inject_node(open_preview) 90 | 91 | Api.node.show_info_popup = inject_node(require("nvim-tree.actions.node.file-popup").toggle_file_info) 92 | Api.node.run.cmd = inject_node(require("nvim-tree.actions.node.run-command").run_file_command) 93 | Api.node.run.system = inject_node(require("nvim-tree.actions.node.system-open").fn) 94 | Api.node.navigate.sibling.next = inject_node(require("nvim-tree.actions.moves.sibling").fn "next") 95 | Api.node.navigate.sibling.prev = inject_node(require("nvim-tree.actions.moves.sibling").fn "prev") 96 | Api.node.navigate.sibling.first = inject_node(require("nvim-tree.actions.moves.sibling").fn "first") 97 | Api.node.navigate.sibling.last = inject_node(require("nvim-tree.actions.moves.sibling").fn "last") 98 | Api.node.navigate.parent = inject_node(require("nvim-tree.actions.moves.parent").fn(false)) 99 | Api.node.navigate.parent_close = inject_node(require("nvim-tree.actions.moves.parent").fn(true)) 100 | Api.node.navigate.git.next = inject_node(require("nvim-tree.actions.moves.item").fn("next", "git")) 101 | Api.node.navigate.git.prev = inject_node(require("nvim-tree.actions.moves.item").fn("prev", "git")) 102 | Api.node.navigate.diagnostics.next = inject_node(require("nvim-tree.actions.moves.item").fn("next", "diag")) 103 | Api.node.navigate.diagnostics.prev = inject_node(require("nvim-tree.actions.moves.item").fn("prev", "diag")) 104 | 105 | Api.git.reload = require("nvim-tree.actions.reloaders.reloaders").reload_git 106 | 107 | Api.events.subscribe = require("nvim-tree.events").subscribe 108 | Api.events.Event = require("nvim-tree.events").Event 109 | 110 | Api.live_filter.start = require("nvim-tree.live-filter").start_filtering 111 | Api.live_filter.clear = require("nvim-tree.live-filter").clear_filter 112 | 113 | Api.marks.get = inject_node(require("nvim-tree.marks").get_mark) 114 | Api.marks.list = require("nvim-tree.marks").get_marks 115 | Api.marks.toggle = inject_node(require("nvim-tree.marks").toggle_mark) 116 | Api.marks.bulk.move = require("nvim-tree.marks.bulk-move").bulk_move 117 | Api.marks.navigate.next = require("nvim-tree.marks.navigation").next 118 | Api.marks.navigate.prev = require("nvim-tree.marks.navigation").prev 119 | Api.marks.navigate.select = require("nvim-tree.marks.navigation").select 120 | 121 | return Api 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A File Explorer For Neovim Written In Lua 2 | 3 | [![CI](https://github.com/kyazdani42/nvim-tree.lua/actions/workflows/ci.yml/badge.svg)](https://github.com/kyazdani42/nvim-tree.lua/actions/workflows/ci.yml) 4 | 5 | 6 | 7 | 8 | Automatic updates 9 | 10 | File type icons 11 | 12 | Git integration 13 | 14 | Diagnostics integration: LSP and COC 15 | 16 | (Live) filtering 17 | 18 | Cut, copy, paste, rename, delete, create 19 | 20 | Highly customisable 21 | 22 |
23 |
24 | 25 | [Join us on matrix](https://matrix.to/#/#nvim-tree:matrix.org) 26 | 27 | ## Requirements 28 | 29 | [neovim >=0.7.0](https://github.com/neovim/neovim/wiki/Installing-Neovim) 30 | 31 | [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons) is optional and used to display file icons. It requires a [patched font](https://www.nerdfonts.com/). 32 | 33 | ## Install 34 | 35 | Install with [vim-plug](https://github.com/junegunn/vim-plug): 36 | 37 | ```vim 38 | Plug 'kyazdani42/nvim-web-devicons' " optional, for file icons 39 | Plug 'kyazdani42/nvim-tree.lua' 40 | ``` 41 | 42 | or with [packer](https://github.com/wbthomason/packer.nvim): 43 | 44 | ```lua 45 | use { 46 | 'kyazdani42/nvim-tree.lua', 47 | requires = { 48 | 'kyazdani42/nvim-web-devicons', -- optional, for file icons 49 | }, 50 | tag = 'nightly' -- optional, updated every week. (see issue #1193) 51 | } 52 | ``` 53 | 54 | ## Setup 55 | 56 | Setup should be run in a lua file or in a lua heredoc [:help lua-heredoc](https://neovim.io/doc/user/lua.html) if using in a vim file. 57 | 58 | ```lua 59 | -- examples for your init.lua 60 | 61 | -- disable netrw at the very start of your init.lua (strongly advised) 62 | vim.g.loaded = 1 63 | vim.g.loaded_netrwPlugin = 1 64 | 65 | -- empty setup using defaults 66 | require("nvim-tree").setup() 67 | 68 | -- OR setup with some options 69 | require("nvim-tree").setup({ 70 | sort_by = "case_sensitive", 71 | view = { 72 | adaptive_size = true, 73 | mappings = { 74 | list = { 75 | { key = "u", action = "dir_up" }, 76 | }, 77 | }, 78 | }, 79 | renderer = { 80 | group_empty = true, 81 | }, 82 | filters = { 83 | dotfiles = true, 84 | }, 85 | }) 86 | ``` 87 | 88 | For complete list of available configuration options see [:help nvim-tree-setup](doc/nvim-tree-lua.txt) 89 | 90 | Each option is documented in `:help nvim-tree.OPTION_NAME`. Nested options can be accessed by appending `.`, for example [:help nvim-tree.view.mappings](doc/nvim-tree-lua.txt) 91 | 92 | ## Commands 93 | 94 | See [:help nvim-tree-commands](doc/nvim-tree-lua.txt) 95 | 96 | Basic commands: 97 | 98 | `:NvimTreeToggle` Open or close the tree. Takes an optional path argument. 99 | 100 | `:NvimTreeFocus` Open the tree if it is closed, and then focus on the tree. 101 | 102 | `:NvimTreeFindFile` Move the cursor in the tree for the current buffer, opening folders if needed. 103 | 104 | `:NvimTreeCollapse` Collapses the nvim-tree recursively. 105 | 106 | ## Api 107 | 108 | nvim-tree exposes a public api; see [:help nvim-tree-api](doc/nvim-tree-lua.txt). This is a stable non breaking api. 109 | 110 | ## Mappings 111 | 112 | nvim-tree comes with number of mappings; for default mappings please see [:help nvim-tree-default-mappings](doc/nvim-tree-lua.txt), for way of configuring mappings see [:help nvim-tree-mappings](doc/nvim-tree-lua.txt) 113 | 114 | `g?` toggles help, showing all the mappings and their actions. 115 | 116 | ## Tips & tricks 117 | 118 | * You can add a directory by adding a `/` at the end of the paths, entering multiple directories `BASE/foo/bar/baz` will add directory foo, then bar and add a file baz to it. 119 | * You can update window options for the tree by setting `require"nvim-tree.view".View.winopts.MY_OPTION = MY_OPTION_VALUE` 120 | * `toggle` has a second parameter which allows to toggle without focusing the explorer (`require"nvim-tree".toggle(false, true)`). 121 | * You can allow nvim-tree to behave like vinegar, see [:help nvim-tree-vinegar](doc/nvim-tree-lua.txt) 122 | * If you `:set nosplitright`, the files will open on the left side of the tree, placing the tree window in the right side of the file you opened. 123 | * You can automatically close the tab/vim when nvim-tree is the last window in the tab: . WARNING: this can catastrophically fail: . This will not be added to nvim-tree and the team will not provide support / assistance with this, due to complexities in vim event timings and side-effects. 124 | * Hide the `.git` folder: `filters = { custom = { "^.git$" } }`. See [:help nvim-tree.filters.custom](doc/nvim-tree-lua.txt). 125 | * To disable the display of icons see [:help nvim-tree.renderer.icons.show](doc/nvim-tree-lua.txt). 126 | 127 | ## Troubleshooting 128 | 129 | ## Diagnostic Logging 130 | 131 | You may enable diagnostic logging to `$XDG_CACHE_HOME/nvim/nvim-tree.log`. See [:help nvim-tree.log](doc/nvim-tree-lua.txt) 132 | 133 | ## netrw Keeps Popping Up 134 | 135 | Eagerly disable netrw. See [:help nvim-tree.disable_netrw](doc/nvim-tree-lua.txt) 136 | 137 | ## Performance Issues 138 | 139 | If you are experiencing performance issues with nvim-tree.lua, you can enable profiling in the logs. It is advisable to enable git logging at the same time, as that can be a source of performance problems. 140 | 141 | ```lua 142 | log = { 143 | enable = true, 144 | truncate = true, 145 | types = { 146 | git = true, 147 | profile = true, 148 | }, 149 | }, 150 | ``` 151 | 152 | Please attach `$XDG_CACHE_HOME/nvim/nvim-tree.log` if you raise an issue. 153 | 154 | *Performance Tips:* 155 | 156 | * If you are using fish as an editor shell (which might be fixed in the future), try set `shell=/bin/bash` in your vim config. Alternatively, you can [prevent fish from loading interactive configuration in a non-interactive shell](https://github.com/kyazdani42/nvim-tree.lua/issues/549#issuecomment-1127394585). 157 | 158 | * Try manually running the git command (see the logs) in your shell e.g. `git --no-optional-locks status --porcelain=v1 --ignored=matching -u`. 159 | 160 | * Huge git repositories may timeout after the default `git.timeout` of 400ms. Try increasing that in your setup if you see `[git] job timed out` in the logs. 161 | 162 | * Try temporarily disabling git integration by setting `git.enable = false` in your setup. 163 | 164 | ## Contributing 165 | 166 | PRs are always welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) 167 | 168 | ### Help Wanted 169 | 170 | Developers with the following environments: 171 | 172 | * Apple macOS 173 | * Windows 174 | * WSL 175 | * msys 176 | * powershell 177 | 178 | Help triaging, diagnosing and fixing issues specific to those environments is needed, as the nvim-tree developers do not have access to or expertise in these environments. 179 | 180 | Let us know you're interested by commenting on issues and raising PRs. 181 | 182 | ## Screenshots 183 | 184 | ![alt text](.github/screenshot.png?raw=true "kyazdani42 tree") 185 | ![alt text](.github/screenshot2.png?raw=true "akin909 tree") 186 | ![alt text](.github/screenshot3.png?raw=true "stsewd tree") 187 | ![alt text](.github/screenshot4.png?raw=true "reyhankaplan tree") 188 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/fs/copy-paste.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | 3 | local lib = require "nvim-tree.lib" 4 | local log = require "nvim-tree.log" 5 | local utils = require "nvim-tree.utils" 6 | local core = require "nvim-tree.core" 7 | 8 | local M = {} 9 | 10 | local clipboard = { 11 | move = {}, 12 | copy = {}, 13 | } 14 | 15 | local function do_copy(source, destination) 16 | local source_stats, handle 17 | local success, errmsg 18 | 19 | source_stats, errmsg = uv.fs_stat(source) 20 | if not source_stats then 21 | log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg) 22 | return false, errmsg 23 | end 24 | 25 | log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination) 26 | 27 | if source == destination then 28 | log.line("copy_paste", "do_copy source and destination are the same, exiting early") 29 | return true 30 | end 31 | 32 | if source_stats.type == "file" then 33 | success, errmsg = uv.fs_copyfile(source, destination) 34 | if not success then 35 | log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg) 36 | return false, errmsg 37 | end 38 | return true 39 | elseif source_stats.type == "directory" then 40 | handle, errmsg = uv.fs_scandir(source) 41 | if type(handle) == "string" then 42 | return false, handle 43 | elseif not handle then 44 | log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, errmsg) 45 | return false, errmsg 46 | end 47 | 48 | success, errmsg = uv.fs_mkdir(destination, source_stats.mode) 49 | if not success then 50 | log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg) 51 | return false, errmsg 52 | end 53 | 54 | while true do 55 | local name, _ = uv.fs_scandir_next(handle) 56 | if not name then 57 | break 58 | end 59 | 60 | local new_name = utils.path_join { source, name } 61 | local new_destination = utils.path_join { destination, name } 62 | success, errmsg = do_copy(new_name, new_destination) 63 | if not success then 64 | return false, errmsg 65 | end 66 | end 67 | else 68 | errmsg = string.format("'%s' illegal file type '%s'", source, source_stats.type) 69 | log.line("copy_paste", "do_copy %s", errmsg) 70 | return false, errmsg 71 | end 72 | 73 | return true 74 | end 75 | 76 | local function do_single_paste(source, dest, action_type, action_fn) 77 | local dest_stats 78 | local success, errmsg, errcode 79 | 80 | log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest) 81 | 82 | dest_stats, errmsg, errcode = uv.fs_stat(dest) 83 | if not dest_stats and errcode ~= "ENOENT" then 84 | utils.notify.error("Could not " .. action_type .. " " .. source .. " - " .. (errmsg or "???")) 85 | return false, errmsg 86 | end 87 | 88 | local function on_process() 89 | success, errmsg = action_fn(source, dest) 90 | if not success then 91 | utils.notify.error("Could not " .. action_type .. " " .. source .. " - " .. (errmsg or "???")) 92 | return false, errmsg 93 | end 94 | end 95 | 96 | if dest_stats then 97 | local prompt_select = "Overwrite " .. dest .. " ?" 98 | local prompt_input = prompt_select .. " y/n/r(ename): " 99 | lib.prompt(prompt_input, prompt_select, { "y", "n", "r" }, { "Yes", "No", "Rename" }, function(item_short) 100 | utils.clear_prompt() 101 | if item_short == "y" then 102 | on_process() 103 | elseif item_short == "r" then 104 | vim.ui.input({ prompt = "Rename to ", default = dest, completion = "dir" }, function(new_dest) 105 | utils.clear_prompt() 106 | if new_dest then 107 | do_single_paste(source, new_dest, action_type, action_fn) 108 | end 109 | end) 110 | end 111 | end) 112 | else 113 | on_process() 114 | end 115 | end 116 | 117 | local function add_to_clipboard(node, clip) 118 | if node.name == ".." then 119 | return 120 | end 121 | 122 | for idx, _node in ipairs(clip) do 123 | if _node.absolute_path == node.absolute_path then 124 | table.remove(clip, idx) 125 | return utils.notify.info(node.absolute_path .. " removed to clipboard.") 126 | end 127 | end 128 | table.insert(clip, node) 129 | utils.notify.info(node.absolute_path .. " added to clipboard.") 130 | end 131 | 132 | function M.copy(node) 133 | add_to_clipboard(node, clipboard.copy) 134 | end 135 | 136 | function M.cut(node) 137 | add_to_clipboard(node, clipboard.move) 138 | end 139 | 140 | local function do_paste(node, action_type, action_fn) 141 | node = lib.get_last_group_node(node) 142 | if node.name == ".." then 143 | return 144 | end 145 | local clip = clipboard[action_type] 146 | if #clip == 0 then 147 | return 148 | end 149 | 150 | local destination = node.absolute_path 151 | local stats, errmsg, errcode = uv.fs_stat(destination) 152 | if not stats and errcode ~= "ENOENT" then 153 | log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, errmsg) 154 | utils.notify.error("Could not " .. action_type .. " " .. destination .. " - " .. (errmsg or "???")) 155 | return 156 | end 157 | local is_dir = stats and stats.type == "directory" 158 | 159 | if not is_dir then 160 | destination = vim.fn.fnamemodify(destination, ":p:h") 161 | elseif not node.open then 162 | destination = vim.fn.fnamemodify(destination, ":p:h:h") 163 | end 164 | 165 | for _, _node in ipairs(clip) do 166 | local dest = utils.path_join { destination, _node.name } 167 | do_single_paste(_node.absolute_path, dest, action_type, action_fn) 168 | end 169 | 170 | clipboard[action_type] = {} 171 | if M.enable_reload then 172 | return require("nvim-tree.actions.reloaders.reloaders").reload_explorer() 173 | end 174 | end 175 | 176 | local function do_cut(source, destination) 177 | log.line("copy_paste", "do_cut '%s' -> '%s'", source, destination) 178 | 179 | if source == destination then 180 | log.line("copy_paste", "do_cut source and destination are the same, exiting early") 181 | return true 182 | end 183 | 184 | local success, errmsg = uv.fs_rename(source, destination) 185 | if not success then 186 | log.line("copy_paste", "do_cut fs_rename failed '%s'", errmsg) 187 | return false, errmsg 188 | end 189 | utils.rename_loaded_buffers(source, destination) 190 | return true 191 | end 192 | 193 | function M.paste(node) 194 | if clipboard.move[1] ~= nil then 195 | return do_paste(node, "move", do_cut) 196 | end 197 | 198 | return do_paste(node, "copy", do_copy) 199 | end 200 | 201 | function M.print_clipboard() 202 | local content = {} 203 | if #clipboard.move > 0 then 204 | table.insert(content, "Cut") 205 | for _, item in pairs(clipboard.move) do 206 | table.insert(content, " * " .. item.absolute_path) 207 | end 208 | end 209 | if #clipboard.copy > 0 then 210 | table.insert(content, "Copy") 211 | for _, item in pairs(clipboard.copy) do 212 | table.insert(content, " * " .. item.absolute_path) 213 | end 214 | end 215 | 216 | return utils.notify.info(table.concat(content, "\n") .. "\n") 217 | end 218 | 219 | local function copy_to_clipboard(content) 220 | if M.use_system_clipboard == true then 221 | vim.fn.setreg("+", content) 222 | vim.fn.setreg('"', content) 223 | return utils.notify.info(string.format("Copied %s to system clipboard!", content)) 224 | else 225 | vim.fn.setreg('"', content) 226 | vim.fn.setreg("1", content) 227 | return utils.notify.info(string.format("Copied %s to neovim clipboard!", content)) 228 | end 229 | end 230 | 231 | function M.copy_filename(node) 232 | return copy_to_clipboard(node.name) 233 | end 234 | 235 | function M.copy_path(node) 236 | local absolute_path = node.absolute_path 237 | local relative_path = utils.path_relative(absolute_path, core.get_cwd()) 238 | local content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path 239 | return copy_to_clipboard(content) 240 | end 241 | 242 | function M.copy_absolute_path(node) 243 | local absolute_path = node.absolute_path 244 | local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path 245 | return copy_to_clipboard(content) 246 | end 247 | 248 | function M.setup(opts) 249 | M.use_system_clipboard = opts.actions.use_system_clipboard 250 | M.enable_reload = not opts.filesystem_watchers.enable 251 | end 252 | 253 | return M 254 | -------------------------------------------------------------------------------- /lua/nvim-tree/keymap.lua: -------------------------------------------------------------------------------- 1 | local Api = require "nvim-tree.api" 2 | 3 | local M = {} 4 | 5 | local DEFAULT_KEYMAPS = { 6 | { 7 | key = { "", "o", "<2-LeftMouse>" }, 8 | callback = Api.node.open.edit, 9 | desc = "open a file or folder; root will cd to the above directory", 10 | }, 11 | { 12 | key = "", 13 | callback = Api.node.open.replace_tree_buffer, 14 | desc = "edit the file in place, effectively replacing the tree explorer", 15 | }, 16 | { 17 | key = "O", 18 | callback = Api.node.open.no_window_picker, 19 | desc = "same as (edit) with no window picker", 20 | }, 21 | { 22 | key = { "", "<2-RightMouse>" }, 23 | callback = Api.tree.change_root_to_node, 24 | desc = "cd in the directory under the cursor", 25 | }, 26 | { 27 | key = "", 28 | callback = Api.node.open.vertical, 29 | desc = "open the file in a vertical split", 30 | }, 31 | { 32 | key = "", 33 | callback = Api.node.open.horizontal, 34 | desc = "open the file in a horizontal split", 35 | }, 36 | { 37 | key = "", 38 | callback = Api.node.open.tab, 39 | desc = "open the file in a new tab", 40 | }, 41 | { 42 | key = "<", 43 | callback = Api.node.navigate.sibling.prev, 44 | desc = "navigate to the previous sibling of current file/directory", 45 | }, 46 | { 47 | key = ">", 48 | callback = Api.node.navigate.sibling.next, 49 | desc = "navigate to the next sibling of current file/directory", 50 | }, 51 | { 52 | key = "P", 53 | callback = Api.node.navigate.parent, 54 | desc = "move cursor to the parent directory", 55 | }, 56 | { 57 | key = "", 58 | callback = Api.node.navigate.parent_close, 59 | desc = "close current opened directory or parent", 60 | }, 61 | { 62 | key = "", 63 | callback = Api.node.open.preview, 64 | desc = "open the file as a preview (keeps the cursor in the tree)", 65 | }, 66 | { 67 | key = "K", 68 | callback = Api.node.navigate.sibling.first, 69 | desc = "navigate to the first sibling of current file/directory", 70 | }, 71 | { 72 | key = "J", 73 | callback = Api.node.navigate.sibling.last, 74 | desc = "navigate to the last sibling of current file/directory", 75 | }, 76 | { 77 | key = "I", 78 | callback = Api.tree.toggle_gitignore_filter, 79 | desc = "toggle visibility of files/folders hidden via |git.ignore| option", 80 | }, 81 | { 82 | key = "H", 83 | callback = Api.tree.toggle_hidden_filter, 84 | desc = "toggle visibility of dotfiles via |filters.dotfiles| option", 85 | }, 86 | { 87 | key = "U", 88 | callback = Api.tree.toggle_custom_filter, 89 | desc = "toggle visibility of files/folders hidden via |filters.custom| option", 90 | }, 91 | { 92 | key = "R", 93 | callback = Api.tree.reload, 94 | desc = "refresh the tree", 95 | }, 96 | { 97 | key = "a", 98 | callback = Api.fs.create, 99 | desc = "add a file; leaving a trailing `/` will add a directory", 100 | }, 101 | { 102 | key = "d", 103 | callback = Api.fs.remove, 104 | desc = "delete a file (will prompt for confirmation)", 105 | }, 106 | { 107 | key = "D", 108 | callback = Api.fs.trash, 109 | desc = "trash a file via |trash| option", 110 | }, 111 | { 112 | key = "r", 113 | callback = Api.fs.rename, 114 | desc = "rename a file", 115 | }, 116 | { 117 | key = "", 118 | callback = Api.fs.rename_sub, 119 | desc = "rename a file and omit the filename on input", 120 | }, 121 | { 122 | key = "x", 123 | callback = Api.fs.cut, 124 | desc = "add/remove file/directory to cut clipboard", 125 | }, 126 | { 127 | key = "c", 128 | callback = Api.fs.copy.node, 129 | desc = "add/remove file/directory to copy clipboard", 130 | }, 131 | { 132 | key = "p", 133 | callback = Api.fs.paste, 134 | desc = "paste from clipboard; cut clipboard has precedence over copy; will prompt for confirmation", 135 | }, 136 | { 137 | key = "y", 138 | callback = Api.fs.copy.filename, 139 | desc = "copy name to system clipboard", 140 | }, 141 | { 142 | key = "Y", 143 | callback = Api.fs.copy.relative_path, 144 | desc = "copy relative path to system clipboard", 145 | }, 146 | { 147 | key = "gy", 148 | callback = Api.fs.copy.absolute_path, 149 | desc = "copy absolute path to system clipboard", 150 | }, 151 | { 152 | key = "]e", 153 | callback = Api.node.navigate.diagnostics.next, 154 | desc = "go to next diagnostic item", 155 | }, 156 | { 157 | key = "]c", 158 | callback = Api.node.navigate.git.next, 159 | desc = "go to next git item", 160 | }, 161 | { 162 | key = "[e", 163 | callback = Api.node.navigate.diagnostics.prev, 164 | desc = "go to prev diagnostic item", 165 | }, 166 | { 167 | key = "[c", 168 | callback = Api.node.navigate.git.prev, 169 | desc = "go to prev git item", 170 | }, 171 | { 172 | key = "-", 173 | callback = Api.tree.change_root_to_parent, 174 | desc = "navigate up to the parent directory of the current file/directory", 175 | }, 176 | { 177 | key = "s", 178 | callback = Api.node.run.system, 179 | desc = "open a file with default system application or a folder with default file manager, using |system_open| option", 180 | }, 181 | { 182 | key = "f", 183 | callback = Api.live_filter.start, 184 | desc = "live filter nodes dynamically based on regex matching.", 185 | }, 186 | { 187 | key = "F", 188 | callback = Api.live_filter.clear, 189 | desc = "clear live filter", 190 | }, 191 | { 192 | key = "q", 193 | callback = Api.tree.close, 194 | desc = "close tree window", 195 | }, 196 | { 197 | key = "W", 198 | callback = Api.tree.collapse_all, 199 | desc = "collapse the whole tree", 200 | }, 201 | { 202 | key = "E", 203 | callback = Api.tree.expand_all, 204 | desc = "expand the whole tree, stopping after expanding |callbacks.expand_all.max_folder_discovery| folders; this might hang neovim for a while if running on a big folder", 205 | }, 206 | { 207 | key = "S", 208 | callback = Api.tree.search_node, 209 | desc = "prompt the user to enter a path and then expands the tree to match the path", 210 | }, 211 | { 212 | key = ".", 213 | callback = Api.node.run.cmd, 214 | desc = "enter vim command mode with the file the cursor is on", 215 | }, 216 | { 217 | key = "", 218 | callback = Api.node.show_info_popup, 219 | desc = "toggle a popup with file infos about the file under the cursor", 220 | }, 221 | { 222 | key = "g?", 223 | callback = Api.tree.toggle_help, 224 | desc = "toggle help", 225 | }, 226 | { 227 | key = "m", 228 | callback = Api.marks.toggle, 229 | desc = "Toggle node in bookmarks", 230 | }, 231 | { 232 | key = "bmv", 233 | callback = Api.marks.bulk.move, 234 | desc = "Move all bookmarked nodes into specified location", 235 | }, 236 | } 237 | 238 | function M.set_keymaps(bufnr) 239 | local opts = { noremap = true, silent = true, nowait = true, buffer = bufnr } 240 | for _, km in ipairs(M.keymaps) do 241 | local keys = type(km.key) == "table" and km.key or { km.key } 242 | for _, key in ipairs(keys) do 243 | vim.keymap.set("n", key, km.callback, opts) 244 | end 245 | end 246 | end 247 | 248 | local function filter_default_mappings(keys_to_disable) 249 | local new_map = {} 250 | for _, m in pairs(DEFAULT_KEYMAPS) do 251 | local keys = type(m.key) == "table" and m.key or { m.key } 252 | local reminding_keys = {} 253 | for _, key in pairs(keys) do 254 | local found = false 255 | for _, key_to_disable in pairs(keys_to_disable) do 256 | if key_to_disable == key then 257 | found = true 258 | break 259 | end 260 | end 261 | if not found then 262 | table.insert(reminding_keys, key) 263 | end 264 | end 265 | if #reminding_keys > 0 then 266 | local map = vim.deepcopy(m) 267 | map.key = reminding_keys 268 | table.insert(new_map, map) 269 | end 270 | end 271 | return new_map 272 | end 273 | 274 | local function get_keymaps(keys_to_disable) 275 | if keys_to_disable == true then 276 | return {} 277 | end 278 | 279 | if type(keys_to_disable) == "table" and #keys_to_disable > 0 then 280 | return filter_default_mappings(keys_to_disable) 281 | end 282 | 283 | return DEFAULT_KEYMAPS 284 | end 285 | 286 | function M.setup(opts) 287 | M.keymaps = get_keymaps(opts.remove_keymaps) 288 | end 289 | 290 | return M 291 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/node/open-file.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2019 Yazdani Kiyan under MIT License 2 | local api = vim.api 3 | 4 | local lib = require "nvim-tree.lib" 5 | local utils = require "nvim-tree.utils" 6 | local view = require "nvim-tree.view" 7 | 8 | local M = {} 9 | 10 | local function get_user_input_char() 11 | local c = vim.fn.getchar() 12 | while type(c) ~= "number" do 13 | c = vim.fn.getchar() 14 | end 15 | return vim.fn.nr2char(c) 16 | end 17 | 18 | ---Get user to pick a window. Selectable windows are all windows in the current 19 | ---tabpage that aren't NvimTree. 20 | ---@return integer|nil -- If a valid window was picked, return its id. If an 21 | --- invalid window was picked / user canceled, return nil. If there are 22 | --- no selectable windows, return -1. 23 | local function pick_window() 24 | local tabpage = api.nvim_get_current_tabpage() 25 | local win_ids = api.nvim_tabpage_list_wins(tabpage) 26 | local tree_winid = view.get_winnr(tabpage) 27 | 28 | local selectable = vim.tbl_filter(function(id) 29 | local bufid = api.nvim_win_get_buf(id) 30 | for option, v in pairs(M.window_picker.exclude) do 31 | local ok, option_value = pcall(api.nvim_buf_get_option, bufid, option) 32 | if ok and vim.tbl_contains(v, option_value) then 33 | return false 34 | end 35 | end 36 | 37 | local win_config = api.nvim_win_get_config(id) 38 | return id ~= tree_winid and win_config.focusable and not win_config.external 39 | end, win_ids) 40 | 41 | -- If there are no selectable windows: return. If there's only 1, return it without picking. 42 | if #selectable == 0 then 43 | return -1 44 | end 45 | if #selectable == 1 then 46 | return selectable[1] 47 | end 48 | 49 | local i = 1 50 | local win_opts = {} 51 | local win_map = {} 52 | local laststatus = vim.o.laststatus 53 | vim.o.laststatus = 2 54 | 55 | local not_selectable = vim.tbl_filter(function(id) 56 | return not vim.tbl_contains(selectable, id) 57 | end, win_ids) 58 | 59 | if laststatus == 3 then 60 | for _, win_id in ipairs(not_selectable) do 61 | local ok_status, statusline = pcall(api.nvim_win_get_option, win_id, "statusline") 62 | local ok_hl, winhl = pcall(api.nvim_win_get_option, win_id, "winhl") 63 | 64 | win_opts[win_id] = { 65 | statusline = ok_status and statusline or "", 66 | winhl = ok_hl and winhl or "", 67 | } 68 | 69 | -- Clear statusline for windows not selectable 70 | api.nvim_win_set_option(win_id, "statusline", " ") 71 | end 72 | end 73 | 74 | -- Setup UI 75 | for _, id in ipairs(selectable) do 76 | local char = M.window_picker.chars:sub(i, i) 77 | local ok_status, statusline = pcall(api.nvim_win_get_option, id, "statusline") 78 | local ok_hl, winhl = pcall(api.nvim_win_get_option, id, "winhl") 79 | 80 | win_opts[id] = { 81 | statusline = ok_status and statusline or "", 82 | winhl = ok_hl and winhl or "", 83 | } 84 | win_map[char] = id 85 | 86 | api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") 87 | api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") 88 | 89 | i = i + 1 90 | if i > #M.window_picker.chars then 91 | break 92 | end 93 | end 94 | 95 | vim.cmd "redraw" 96 | if vim.opt.cmdheight._value ~= 0 then 97 | print "Pick window: " 98 | end 99 | local _, resp = pcall(get_user_input_char) 100 | resp = (resp or ""):upper() 101 | utils.clear_prompt() 102 | 103 | -- Restore window options 104 | for _, id in ipairs(selectable) do 105 | for opt, value in pairs(win_opts[id]) do 106 | api.nvim_win_set_option(id, opt, value) 107 | end 108 | end 109 | 110 | if laststatus == 3 then 111 | for _, id in ipairs(not_selectable) do 112 | for opt, value in pairs(win_opts[id]) do 113 | api.nvim_win_set_option(id, opt, value) 114 | end 115 | end 116 | end 117 | 118 | vim.o.laststatus = laststatus 119 | 120 | if not vim.tbl_contains(vim.split(M.window_picker.chars, ""), resp) then 121 | return 122 | end 123 | 124 | return win_map[resp] 125 | end 126 | 127 | local function open_file_in_tab(filename) 128 | if M.quit_on_open then 129 | view.close() 130 | end 131 | vim.cmd("tabe " .. vim.fn.fnameescape(filename)) 132 | end 133 | 134 | local function on_preview(buf_loaded) 135 | if not buf_loaded then 136 | vim.bo.bufhidden = "delete" 137 | 138 | api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { 139 | group = api.nvim_create_augroup("RemoveBufHidden", {}), 140 | buffer = api.nvim_get_current_buf(), 141 | callback = function() 142 | vim.bo.bufhidden = "" 143 | end, 144 | once = true, 145 | }) 146 | end 147 | view.focus() 148 | end 149 | 150 | local function get_target_winid(mode) 151 | local target_winid 152 | if not M.window_picker.enable or mode == "edit_no_picker" then 153 | target_winid = lib.target_winid 154 | else 155 | local pick_window_id = pick_window() 156 | if pick_window_id == nil then 157 | return 158 | end 159 | target_winid = pick_window_id 160 | end 161 | 162 | if target_winid == -1 then 163 | target_winid = lib.target_winid 164 | end 165 | return target_winid 166 | end 167 | 168 | -- This is only to avoid the BufEnter for nvim-tree to trigger 169 | -- which would cause find-file to run on an invalid file. 170 | local function set_current_win_no_autocmd(winid) 171 | vim.cmd "set ei=BufEnter" 172 | api.nvim_set_current_win(winid) 173 | vim.cmd 'set ei=""' 174 | end 175 | 176 | local function open_in_new_window(filename, mode, win_ids) 177 | local target_winid = get_target_winid(mode) 178 | if not target_winid then 179 | return 180 | end 181 | local do_split = mode == "split" or mode == "vsplit" 182 | local split_side = (view.View.side == "right") and "aboveleft" or "belowright" 183 | 184 | -- Target is invalid or window does not exist in current tabpage: create new window 185 | if not target_winid or not vim.tbl_contains(win_ids, target_winid) then 186 | vim.cmd(split_side .. " vsplit") 187 | target_winid = api.nvim_get_current_win() 188 | lib.target_winid = target_winid 189 | 190 | -- No need to split, as we created a new window. 191 | do_split = false 192 | elseif not vim.o.hidden then 193 | -- If `hidden` is not enabled, check if buffer in target window is 194 | -- modified, and create new split if it is. 195 | local target_bufid = api.nvim_win_get_buf(target_winid) 196 | if api.nvim_buf_get_option(target_bufid, "modified") then 197 | do_split = true 198 | end 199 | end 200 | 201 | local fname = vim.fn.fnameescape(filename) 202 | 203 | local cmd 204 | if do_split or #api.nvim_list_wins() == 1 then 205 | local split_cmd = (mode ~= "split") and "vsplit" or "split" 206 | cmd = string.format("%s %s %s", split_side, split_cmd, fname) 207 | else 208 | cmd = string.format("edit %s", fname) 209 | end 210 | 211 | set_current_win_no_autocmd(target_winid) 212 | pcall(vim.cmd, cmd) 213 | lib.set_target_win() 214 | end 215 | 216 | local function is_already_loaded(filename) 217 | for _, buf_id in ipairs(api.nvim_list_bufs()) do 218 | if api.nvim_buf_is_loaded(buf_id) and filename == api.nvim_buf_get_name(buf_id) then 219 | return true 220 | end 221 | end 222 | return false 223 | end 224 | 225 | local function edit_in_current_buf(filename) 226 | require("nvim-tree.view").abandon_current_window() 227 | vim.cmd("edit " .. vim.fn.fnameescape(filename)) 228 | end 229 | 230 | function M.fn(mode, filename) 231 | if mode == "tabnew" then 232 | return open_file_in_tab(filename) 233 | end 234 | 235 | if mode == "edit_in_place" then 236 | return edit_in_current_buf(filename) 237 | end 238 | 239 | local tabpage = api.nvim_get_current_tabpage() 240 | local win_ids = api.nvim_tabpage_list_wins(tabpage) 241 | local buf_loaded = is_already_loaded(filename) 242 | 243 | local found_win = utils.get_win_buf_from_path(filename) 244 | if found_win and mode == "preview" then 245 | return 246 | end 247 | 248 | if not found_win then 249 | open_in_new_window(filename, mode, win_ids) 250 | else 251 | api.nvim_set_current_win(found_win) 252 | vim.bo.bufhidden = "" 253 | end 254 | 255 | if M.resize_window then 256 | view.resize() 257 | end 258 | 259 | if mode == "preview" then 260 | return on_preview(buf_loaded) 261 | end 262 | 263 | if M.quit_on_open then 264 | view.close() 265 | end 266 | end 267 | 268 | function M.setup(opts) 269 | M.quit_on_open = opts.actions.open_file.quit_on_open or opts.view.float.enable 270 | M.resize_window = opts.actions.open_file.resize_window 271 | if opts.actions.open_file.window_picker.chars then 272 | opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper() 273 | end 274 | M.window_picker = opts.actions.open_file.window_picker 275 | end 276 | 277 | return M 278 | -------------------------------------------------------------------------------- /lua/nvim-tree/renderer/builder.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | local core = require "nvim-tree.core" 3 | 4 | local git = require "nvim-tree.renderer.components.git" 5 | local pad = require "nvim-tree.renderer.components.padding" 6 | local icons = require "nvim-tree.renderer.components.icons" 7 | 8 | local Builder = {} 9 | Builder.__index = Builder 10 | 11 | function Builder.new(root_cwd) 12 | return setmetatable({ 13 | index = 0, 14 | depth = 0, 15 | highlights = {}, 16 | lines = {}, 17 | markers = {}, 18 | signs = {}, 19 | root_cwd = root_cwd, 20 | }, Builder) 21 | end 22 | 23 | function Builder:configure_root_modifier(root_folder_modifier) 24 | self.root_folder_modifier = root_folder_modifier or ":~" 25 | return self 26 | end 27 | 28 | function Builder:configure_trailing_slash(with_trailing) 29 | self.trailing_slash = with_trailing and "/" or "" 30 | return self 31 | end 32 | 33 | function Builder:configure_special_files(special_files) 34 | self.special_files = special_files 35 | return self 36 | end 37 | 38 | function Builder:configure_picture_map(picture_map) 39 | self.picture_map = picture_map 40 | return self 41 | end 42 | 43 | function Builder:configure_filter(filter, prefix) 44 | self.filter_prefix = prefix 45 | self.filter = filter 46 | return self 47 | end 48 | 49 | function Builder:configure_opened_file_highlighting(highlight_opened_files) 50 | self.highlight_opened_files = highlight_opened_files 51 | 52 | return self 53 | end 54 | 55 | function Builder:configure_git_icons_padding(padding) 56 | self.git_icon_padding = padding or " " 57 | return self 58 | end 59 | 60 | function Builder:configure_git_icons_placement(where) 61 | if where == "signcolumn" then 62 | vim.fn.sign_unplace(git.SIGN_GROUP) 63 | self.is_git_sign = true 64 | end 65 | self.is_git_after = where == "after" and not self.is_git_sign 66 | return self 67 | end 68 | 69 | function Builder:configure_symlink_destination(show) 70 | self.symlink_destination = show 71 | return self 72 | end 73 | 74 | function Builder:_insert_highlight(group, start, end_) 75 | table.insert(self.highlights, { group, self.index, start, end_ or -1 }) 76 | end 77 | 78 | function Builder:_insert_line(line) 79 | table.insert(self.lines, line) 80 | end 81 | 82 | local function get_folder_name(node) 83 | local name = node.name 84 | local next = node.group_next 85 | while next do 86 | name = name .. "/" .. next.name 87 | next = next.group_next 88 | end 89 | return name 90 | end 91 | 92 | function Builder:_unwrap_git_data(git_icons_and_hl_groups, offset) 93 | if not git_icons_and_hl_groups then 94 | return "" 95 | end 96 | 97 | local icon = "" 98 | for i, v in ipairs(git_icons_and_hl_groups) do 99 | if #v.icon > 0 then 100 | self:_insert_highlight(v.hl, offset + #icon, offset + #icon + #v.icon) 101 | local remove_padding = self.is_git_after and i == #git_icons_and_hl_groups 102 | icon = icon .. v.icon .. (remove_padding and "" or self.git_icon_padding) 103 | end 104 | end 105 | return icon 106 | end 107 | 108 | function Builder:_build_folder(node, padding, git_hl, git_icons_tbl) 109 | local offset = string.len(padding) 110 | 111 | local name = get_folder_name(node) 112 | local has_children = #node.nodes ~= 0 or node.has_children 113 | local icon = icons.get_folder_icon(node.open, node.link_to ~= nil, has_children) 114 | 115 | local foldername = name .. self.trailing_slash 116 | if node.link_to and self.symlink_destination then 117 | local arrow = icons.i.symlink_arrow 118 | local link_to = utils.path_relative(node.link_to, core.get_cwd()) 119 | foldername = foldername .. arrow .. link_to 120 | end 121 | 122 | local git_icons = self:_unwrap_git_data(git_icons_tbl, offset + #icon + (self.is_git_after and #foldername + 1 or 0)) 123 | local fname_starts_at = offset + #icon + (self.is_git_after and 0 or #git_icons) 124 | local line = self:_format_line(padding .. icon, foldername, git_icons) 125 | self:_insert_line(line) 126 | 127 | if #icon > 0 then 128 | self:_insert_highlight("NvimTreeFolderIcon", offset, offset + #icon) 129 | end 130 | 131 | local foldername_hl = "NvimTreeFolderName" 132 | if vim.tbl_contains(self.special_files, node.absolute_path) or vim.tbl_contains(self.special_files, node.name) then 133 | foldername_hl = "NvimTreeSpecialFolderName" 134 | elseif node.open then 135 | foldername_hl = "NvimTreeOpenedFolderName" 136 | elseif not has_children then 137 | foldername_hl = "NvimTreeEmptyFolderName" 138 | end 139 | 140 | self:_insert_highlight(foldername_hl, fname_starts_at, fname_starts_at + #foldername) 141 | 142 | if git_hl then 143 | self:_insert_highlight(git_hl, fname_starts_at, fname_starts_at + #foldername) 144 | end 145 | end 146 | 147 | function Builder:_format_line(before, after, git_icons) 148 | git_icons = self.is_git_after and git_icons and " " .. git_icons or git_icons 149 | return string.format( 150 | "%s%s%s%s", 151 | before, 152 | self.is_git_after and "" or git_icons, 153 | after, 154 | self.is_git_after and git_icons or "" 155 | ) 156 | end 157 | 158 | function Builder:_build_symlink(node, padding, git_highlight, git_icons_tbl) 159 | local offset = string.len(padding) 160 | 161 | local icon = icons.i.symlink 162 | local arrow = icons.i.symlink_arrow 163 | local symlink_formatted = node.name 164 | if self.symlink_destination then 165 | local link_to = utils.path_relative(node.link_to, core.get_cwd()) 166 | symlink_formatted = symlink_formatted .. arrow .. link_to 167 | end 168 | 169 | local link_highlight = git_highlight or "NvimTreeSymlink" 170 | 171 | local git_icons_starts_at = offset + #icon + (self.is_git_after and #symlink_formatted + 1 or 0) 172 | local git_icons = self:_unwrap_git_data(git_icons_tbl, git_icons_starts_at) 173 | local line = self:_format_line(padding .. icon, symlink_formatted, git_icons) 174 | 175 | self:_insert_highlight(link_highlight, offset + (self.is_git_after and 0 or #git_icons), string.len(line)) 176 | self:_insert_line(line) 177 | end 178 | 179 | function Builder:_build_file_icon(node, offset) 180 | local icon, hl_group = icons.get_file_icon(node.name, node.extension) 181 | if hl_group then 182 | self:_insert_highlight(hl_group, offset, offset + #icon) 183 | end 184 | return icon, false 185 | end 186 | 187 | function Builder:_highlight_opened_files(node, offset, icon_length, git_icons_length) 188 | local from = offset 189 | local to = offset 190 | 191 | if self.highlight_opened_files == "icon" then 192 | to = from + icon_length 193 | elseif self.highlight_opened_files == "name" then 194 | from = offset + icon_length + git_icons_length 195 | to = from + #node.name 196 | elseif self.highlight_opened_files == "all" then 197 | to = from + icon_length + git_icons_length + #node.name 198 | end 199 | 200 | self:_insert_highlight("NvimTreeOpenedFile", from, to) 201 | end 202 | 203 | function Builder:_build_file(node, padding, git_highlight, git_icons_tbl) 204 | local offset = string.len(padding) 205 | 206 | local icon = self:_build_file_icon(node, offset) 207 | 208 | local git_icons_starts_at = offset + #icon + (self.is_git_after and #node.name + 1 or 0) 209 | local git_icons = self:_unwrap_git_data(git_icons_tbl, git_icons_starts_at) 210 | 211 | self:_insert_line(self:_format_line(padding .. icon, node.name, git_icons)) 212 | 213 | local git_icons_length = self.is_git_after and 0 or #git_icons 214 | local col_start = offset + #icon + git_icons_length 215 | local col_end = col_start + #node.name 216 | 217 | if vim.tbl_contains(self.special_files, node.absolute_path) or vim.tbl_contains(self.special_files, node.name) then 218 | self:_insert_highlight("NvimTreeSpecialFile", col_start, col_end) 219 | elseif node.executable then 220 | self:_insert_highlight("NvimTreeExecFile", col_start, col_end) 221 | elseif self.picture_map[node.extension] then 222 | self:_insert_highlight("NvimTreeImageFile", col_start, col_end) 223 | end 224 | 225 | local should_highlight_opened_files = self.highlight_opened_files and vim.fn.bufloaded(node.absolute_path) > 0 226 | if should_highlight_opened_files then 227 | self:_highlight_opened_files(node, offset, #icon, git_icons_length) 228 | end 229 | 230 | if git_highlight then 231 | self:_insert_highlight(git_highlight, col_start, col_end) 232 | end 233 | end 234 | 235 | function Builder:_build_line(node, idx, num_children) 236 | local padding = pad.get_padding(self.depth, idx, num_children, node, self.markers) 237 | 238 | if string.len(padding) > 0 then 239 | self:_insert_highlight("NvimTreeIndentMarker", 0, string.len(padding)) 240 | end 241 | 242 | local git_highlight = git.get_highlight(node) 243 | local git_icons_tbl = git.get_icons(node) 244 | 245 | if self.is_git_sign and git_icons_tbl and #git_icons_tbl > 0 then 246 | local git_info = git_icons_tbl[1] 247 | table.insert(self.signs, { sign = git_info.hl, lnum = self.index + 1 }) 248 | git_icons_tbl = {} 249 | end 250 | 251 | local is_folder = node.nodes ~= nil 252 | local is_symlink = node.link_to ~= nil 253 | 254 | if is_folder then 255 | self:_build_folder(node, padding, git_highlight, git_icons_tbl) 256 | elseif is_symlink then 257 | self:_build_symlink(node, padding, git_highlight, git_icons_tbl) 258 | else 259 | self:_build_file(node, padding, git_highlight, git_icons_tbl) 260 | end 261 | self.index = self.index + 1 262 | 263 | if node.open then 264 | self.depth = self.depth + 1 265 | self:build(node) 266 | self.depth = self.depth - 1 267 | end 268 | end 269 | 270 | function Builder:_get_nodes_number(nodes) 271 | if not self.filter then 272 | return #nodes 273 | end 274 | 275 | local i = 0 276 | for _, n in pairs(nodes) do 277 | if not n.hidden then 278 | i = i + 1 279 | end 280 | end 281 | return i 282 | end 283 | 284 | function Builder:build(tree) 285 | local num_children = self:_get_nodes_number(tree.nodes) 286 | local idx = 1 287 | for _, node in ipairs(tree.nodes) do 288 | if not node.hidden then 289 | self:_build_line(node, idx, num_children) 290 | idx = idx + 1 291 | end 292 | end 293 | 294 | return self 295 | end 296 | 297 | local function format_root_name(root_cwd, modifier) 298 | local base_root = utils.path_remove_trailing(vim.fn.fnamemodify(root_cwd, modifier)) 299 | return utils.path_join { base_root, ".." } 300 | end 301 | 302 | function Builder:build_header(show_header) 303 | if show_header then 304 | local root_name = format_root_name(self.root_cwd, self.root_folder_modifier) 305 | self:_insert_line(root_name) 306 | self:_insert_highlight("NvimTreeRootFolder", 0, string.len(root_name)) 307 | self.index = 1 308 | end 309 | 310 | if self.filter then 311 | local filter_line = self.filter_prefix .. "/" .. self.filter .. "/" 312 | self:_insert_line(filter_line) 313 | local prefix_length = string.len(self.filter_prefix) 314 | self:_insert_highlight("NvimTreeLiveFilterPrefix", 0, prefix_length) 315 | self:_insert_highlight("NvimTreeLiveFilterValue", prefix_length, string.len(filter_line)) 316 | self.index = self.index + 1 317 | end 318 | 319 | return self 320 | end 321 | 322 | function Builder:unwrap() 323 | return self.lines, self.highlights, self.signs 324 | end 325 | 326 | return Builder 327 | -------------------------------------------------------------------------------- /lua/nvim-tree/legacy.lua: -------------------------------------------------------------------------------- 1 | local utils = require "nvim-tree.utils" 2 | 3 | local M = {} 4 | 5 | -- TODO update bit.ly/3vIpEOJ when adding a migration 6 | 7 | -- migrate the g: to o if the user has not specified that when calling setup 8 | local g_migrations = { 9 | nvim_tree_disable_netrw = function(o) 10 | if o.disable_netrw == nil then 11 | o.disable_netrw = vim.g.nvim_tree_disable_netrw ~= 0 12 | end 13 | end, 14 | 15 | nvim_tree_hijack_netrw = function(o) 16 | if o.hijack_netrw == nil then 17 | o.hijack_netrw = vim.g.nvim_tree_hijack_netrw ~= 0 18 | end 19 | end, 20 | 21 | nvim_tree_auto_open = function(o) 22 | if o.open_on_setup == nil then 23 | o.open_on_setup = vim.g.nvim_tree_auto_open ~= 0 24 | end 25 | end, 26 | 27 | nvim_tree_tab_open = function(o) 28 | if o.open_on_tab == nil then 29 | o.open_on_tab = vim.g.nvim_tree_tab_open ~= 0 30 | end 31 | end, 32 | 33 | nvim_tree_update_cwd = function(o) 34 | if o.update_cwd == nil then 35 | o.update_cwd = vim.g.nvim_tree_update_cwd ~= 0 36 | end 37 | end, 38 | 39 | nvim_tree_hijack_cursor = function(o) 40 | if o.hijack_cursor == nil then 41 | o.hijack_cursor = vim.g.nvim_tree_hijack_cursor ~= 0 42 | end 43 | end, 44 | 45 | nvim_tree_system_open_command = function(o) 46 | utils.table_create_missing(o, "system_open") 47 | if o.system_open.cmd == nil then 48 | o.system_open.cmd = vim.g.nvim_tree_system_open_command 49 | end 50 | end, 51 | 52 | nvim_tree_system_open_command_args = function(o) 53 | utils.table_create_missing(o, "system_open") 54 | if o.system_open.args == nil then 55 | o.system_open.args = vim.g.nvim_tree_system_open_command_args 56 | end 57 | end, 58 | 59 | nvim_tree_follow = function(o) 60 | utils.table_create_missing(o, "update_focused_file") 61 | if o.update_focused_file.enable == nil then 62 | o.update_focused_file.enable = vim.g.nvim_tree_follow ~= 0 63 | end 64 | end, 65 | 66 | nvim_tree_follow_update_path = function(o) 67 | utils.table_create_missing(o, "update_focused_file") 68 | if o.update_focused_file.update_cwd == nil then 69 | o.update_focused_file.update_cwd = vim.g.nvim_tree_follow_update_path ~= 0 70 | end 71 | end, 72 | 73 | nvim_tree_lsp_diagnostics = function(o) 74 | utils.table_create_missing(o, "diagnostics") 75 | if o.diagnostics.enable == nil then 76 | o.diagnostics.enable = vim.g.nvim_tree_lsp_diagnostics ~= 0 77 | if o.diagnostics.show_on_dirs == nil then 78 | o.diagnostics.show_on_dirs = vim.g.nvim_tree_lsp_diagnostics ~= 0 79 | end 80 | end 81 | end, 82 | 83 | nvim_tree_auto_resize = function(o) 84 | utils.table_create_missing(o, "actions.open_file") 85 | if o.actions.open_file.resize_window == nil then 86 | o.actions.open_file.resize_window = vim.g.nvim_tree_auto_resize ~= 0 87 | end 88 | end, 89 | 90 | nvim_tree_bindings = function(o) 91 | utils.table_create_missing(o, "view.mappings") 92 | if o.view.mappings.list == nil then 93 | o.view.mappings.list = vim.g.nvim_tree_bindings 94 | end 95 | end, 96 | 97 | nvim_tree_disable_keybindings = function(o) 98 | utils.table_create_missing(o, "view.mappings") 99 | if o.view.mappings.custom_only == nil then 100 | if vim.g.nvim_tree_disable_keybindings ~= 0 then 101 | o.view.mappings.custom_only = true 102 | -- specify one mapping so that defaults do not apply 103 | o.view.mappings.list = { 104 | { key = "g?", action = "" }, 105 | } 106 | end 107 | end 108 | end, 109 | 110 | nvim_tree_disable_default_keybindings = function(o) 111 | utils.table_create_missing(o, "view.mappings") 112 | if o.view.mappings.custom_only == nil then 113 | o.view.mappings.custom_only = vim.g.nvim_tree_disable_default_keybindings ~= 0 114 | end 115 | end, 116 | 117 | nvim_tree_hide_dotfiles = function(o) 118 | utils.table_create_missing(o, "filters") 119 | if o.filters.dotfiles == nil then 120 | o.filters.dotfiles = vim.g.nvim_tree_hide_dotfiles ~= 0 121 | end 122 | end, 123 | 124 | nvim_tree_ignore = function(o) 125 | utils.table_create_missing(o, "filters") 126 | if o.filters.custom == nil then 127 | o.filters.custom = vim.g.nvim_tree_ignore 128 | end 129 | end, 130 | 131 | nvim_tree_gitignore = function(o) 132 | utils.table_create_missing(o, "git") 133 | if o.git.ignore == nil then 134 | o.git.ignore = vim.g.nvim_tree_gitignore ~= 0 135 | end 136 | end, 137 | 138 | nvim_tree_disable_window_picker = function(o) 139 | utils.table_create_missing(o, "actions.open_file.window_picker") 140 | if o.actions.open_file.window_picker.enable == nil then 141 | o.actions.open_file.window_picker.enable = vim.g.nvim_tree_disable_window_picker == 0 142 | end 143 | end, 144 | 145 | nvim_tree_window_picker_chars = function(o) 146 | utils.table_create_missing(o, "actions.open_file.window_picker") 147 | if o.actions.open_file.window_picker.chars == nil then 148 | o.actions.open_file.window_picker.chars = vim.g.nvim_tree_window_picker_chars 149 | end 150 | end, 151 | 152 | nvim_tree_window_picker_exclude = function(o) 153 | utils.table_create_missing(o, "actions.open_file.window_picker") 154 | if o.actions.open_file.window_picker.exclude == nil then 155 | o.actions.open_file.window_picker.exclude = vim.g.nvim_tree_window_picker_exclude 156 | end 157 | end, 158 | 159 | nvim_tree_quit_on_open = function(o) 160 | utils.table_create_missing(o, "actions.open_file") 161 | if o.actions.open_file.quit_on_open == nil then 162 | o.actions.open_file.quit_on_open = vim.g.nvim_tree_quit_on_open == 1 163 | end 164 | end, 165 | 166 | nvim_tree_change_dir_global = function(o) 167 | utils.table_create_missing(o, "actions.change_dir") 168 | if o.actions.change_dir.global == nil then 169 | o.actions.change_dir.global = vim.g.nvim_tree_change_dir_global == 1 170 | end 171 | end, 172 | 173 | nvim_tree_indent_markers = function(o) 174 | utils.table_create_missing(o, "renderer.indent_markers") 175 | if o.renderer.indent_markers.enable == nil then 176 | o.renderer.indent_markers.enable = vim.g.nvim_tree_indent_markers == 1 177 | end 178 | end, 179 | 180 | nvim_tree_add_trailing = function(o) 181 | utils.table_create_missing(o, "renderer") 182 | if o.renderer.add_trailing == nil then 183 | o.renderer.add_trailing = vim.g.nvim_tree_add_trailing == 1 184 | end 185 | end, 186 | 187 | nvim_tree_highlight_opened_files = function(o) 188 | utils.table_create_missing(o, "renderer") 189 | if o.renderer.highlight_opened_files == nil then 190 | if vim.g.nvim_tree_highlight_opened_files == 1 then 191 | o.renderer.highlight_opened_files = "icon" 192 | elseif vim.g.nvim_tree_highlight_opened_files == 2 then 193 | o.renderer.highlight_opened_files = "name" 194 | elseif vim.g.nvim_tree_highlight_opened_files == 3 then 195 | o.renderer.highlight_opened_files = "all" 196 | end 197 | end 198 | end, 199 | 200 | nvim_tree_root_folder_modifier = function(o) 201 | utils.table_create_missing(o, "renderer") 202 | if o.renderer.root_folder_modifier == nil then 203 | o.renderer.root_folder_modifier = vim.g.nvim_tree_root_folder_modifier 204 | end 205 | end, 206 | 207 | nvim_tree_special_files = function(o) 208 | utils.table_create_missing(o, "renderer") 209 | if o.renderer.special_files == nil and type(vim.g.nvim_tree_special_files) == "table" then 210 | o.renderer.special_files = {} 211 | for k, v in pairs(vim.g.nvim_tree_special_files) do 212 | if v ~= 0 then 213 | table.insert(o.renderer.special_files, k) 214 | end 215 | end 216 | end 217 | end, 218 | 219 | nvim_tree_icon_padding = function(o) 220 | utils.table_create_missing(o, "renderer.icons") 221 | if o.renderer.icons.padding == nil then 222 | o.renderer.icons.padding = vim.g.nvim_tree_icon_padding 223 | end 224 | end, 225 | 226 | nvim_tree_symlink_arrow = function(o) 227 | utils.table_create_missing(o, "renderer.icons") 228 | if o.renderer.icons.symlink_arrow == nil then 229 | o.renderer.icons.symlink_arrow = vim.g.nvim_tree_symlink_arrow 230 | end 231 | end, 232 | 233 | nvim_tree_show_icons = function(o) 234 | utils.table_create_missing(o, "renderer.icons") 235 | if o.renderer.icons.show == nil and type(vim.g.nvim_tree_show_icons) == "table" then 236 | o.renderer.icons.show = {} 237 | o.renderer.icons.show.file = vim.g.nvim_tree_show_icons.files == 1 238 | o.renderer.icons.show.folder = vim.g.nvim_tree_show_icons.folders == 1 239 | o.renderer.icons.show.folder_arrow = vim.g.nvim_tree_show_icons.folder_arrows == 1 240 | o.renderer.icons.show.git = vim.g.nvim_tree_show_icons.git == 1 241 | end 242 | end, 243 | 244 | nvim_tree_icons = function(o) 245 | utils.table_create_missing(o, "renderer.icons") 246 | if o.renderer.icons.glyphs == nil and type(vim.g.nvim_tree_icons) == "table" then 247 | o.renderer.icons.glyphs = vim.g.nvim_tree_icons 248 | end 249 | end, 250 | 251 | nvim_tree_git_hl = function(o) 252 | utils.table_create_missing(o, "renderer") 253 | if o.renderer.highlight_git == nil then 254 | o.renderer.highlight_git = vim.g.nvim_tree_git_hl == 1 255 | end 256 | end, 257 | 258 | nvim_tree_group_empty = function(o) 259 | utils.table_create_missing(o, "renderer") 260 | if o.renderer.group_empty == nil then 261 | o.renderer.group_empty = vim.g.nvim_tree_group_empty == 1 262 | end 263 | end, 264 | 265 | nvim_tree_respect_buf_cwd = function(o) 266 | if o.respect_buf_cwd == nil then 267 | o.respect_buf_cwd = vim.g.nvim_tree_respect_buf_cwd == 1 268 | end 269 | end, 270 | 271 | nvim_tree_create_in_closed_folder = function(o) 272 | if o.create_in_closed_folder == nil then 273 | o.create_in_closed_folder = vim.g.nvim_tree_create_in_closed_folder == 1 274 | end 275 | end, 276 | } 277 | 278 | local function refactored(opts) 279 | -- mapping actions 280 | if opts.view and opts.view.mappings and opts.view.mappings.list then 281 | for _, m in pairs(opts.view.mappings.list) do 282 | if m.action == "toggle_ignored" then 283 | m.action = "toggle_git_ignored" 284 | end 285 | end 286 | end 287 | 288 | -- 2022/06/20 289 | utils.move_missing_val(opts, "update_focused_file", "update_cwd", opts, "update_focused_file", "update_root") 290 | utils.move_missing_val(opts, "", "update_cwd", opts, "", "sync_root_with_cwd") 291 | end 292 | 293 | local function removed(opts) 294 | if opts.auto_close then 295 | utils.notify.warn "auto close feature has been removed, see note in the README (tips & reminder section)" 296 | opts.auto_close = nil 297 | end 298 | end 299 | 300 | function M.migrate_legacy_options(opts) 301 | -- g: options 302 | local msg 303 | for g, m in pairs(g_migrations) do 304 | if vim.fn.exists("g:" .. g) ~= 0 then 305 | m(opts) 306 | msg = (msg and msg .. ", " or "Following options were moved to setup, see bit.ly/3vIpEOJ: ") .. g 307 | end 308 | end 309 | if msg then 310 | utils.notify.warn(msg) 311 | end 312 | 313 | -- silently move 314 | refactored(opts) 315 | 316 | -- warn and delete 317 | removed(opts) 318 | end 319 | 320 | return M 321 | -------------------------------------------------------------------------------- /lua/nvim-tree/actions/init.lua: -------------------------------------------------------------------------------- 1 | -- @deprecated: new implementation in nvim-tree.keymap. Please do not edit this file. 2 | 3 | local a = vim.api 4 | 5 | local log = require "nvim-tree.log" 6 | local view = require "nvim-tree.view" 7 | local util = require "nvim-tree.utils" 8 | 9 | -- BEGIN_DEFAULT_MAPPINGS 10 | local DEFAULT_MAPPINGS = { 11 | { 12 | key = { "", "o", "<2-LeftMouse>" }, 13 | action = "edit", 14 | desc = "open a file or folder; root will cd to the above directory", 15 | }, 16 | { 17 | key = "", 18 | action = "edit_in_place", 19 | desc = "edit the file in place, effectively replacing the tree explorer", 20 | }, 21 | { 22 | key = "O", 23 | action = "edit_no_picker", 24 | desc = "same as (edit) with no window picker", 25 | }, 26 | { 27 | key = { "", "<2-RightMouse>" }, 28 | action = "cd", 29 | desc = "cd in the directory under the cursor", 30 | }, 31 | { 32 | key = "", 33 | action = "vsplit", 34 | desc = "open the file in a vertical split", 35 | }, 36 | { 37 | key = "", 38 | action = "split", 39 | desc = "open the file in a horizontal split", 40 | }, 41 | { 42 | key = "", 43 | action = "tabnew", 44 | desc = "open the file in a new tab", 45 | }, 46 | { 47 | key = "<", 48 | action = "prev_sibling", 49 | desc = "navigate to the previous sibling of current file/directory", 50 | }, 51 | { 52 | key = ">", 53 | action = "next_sibling", 54 | desc = "navigate to the next sibling of current file/directory", 55 | }, 56 | { 57 | key = "P", 58 | action = "parent_node", 59 | desc = "move cursor to the parent directory", 60 | }, 61 | { 62 | key = "", 63 | action = "close_node", 64 | desc = "close current opened directory or parent", 65 | }, 66 | { 67 | key = "", 68 | action = "preview", 69 | desc = "open the file as a preview (keeps the cursor in the tree)", 70 | }, 71 | { 72 | key = "K", 73 | action = "first_sibling", 74 | desc = "navigate to the first sibling of current file/directory", 75 | }, 76 | { 77 | key = "J", 78 | action = "last_sibling", 79 | desc = "navigate to the last sibling of current file/directory", 80 | }, 81 | { 82 | key = "I", 83 | action = "toggle_git_ignored", 84 | desc = "toggle visibility of files/folders hidden via |git.ignore| option", 85 | }, 86 | { 87 | key = "H", 88 | action = "toggle_dotfiles", 89 | desc = "toggle visibility of dotfiles via |filters.dotfiles| option", 90 | }, 91 | { 92 | key = "U", 93 | action = "toggle_custom", 94 | desc = "toggle visibility of files/folders hidden via |filters.custom| option", 95 | }, 96 | { 97 | key = "R", 98 | action = "refresh", 99 | desc = "refresh the tree", 100 | }, 101 | { 102 | key = "a", 103 | action = "create", 104 | desc = "add a file; leaving a trailing `/` will add a directory", 105 | }, 106 | { 107 | key = "d", 108 | action = "remove", 109 | desc = "delete a file (will prompt for confirmation)", 110 | }, 111 | { 112 | key = "D", 113 | action = "trash", 114 | desc = "trash a file via |trash| option", 115 | }, 116 | { 117 | key = "r", 118 | action = "rename", 119 | desc = "rename a file", 120 | }, 121 | { 122 | key = "", 123 | action = "full_rename", 124 | desc = "rename a file and omit the filename on input", 125 | }, 126 | { 127 | key = "x", 128 | action = "cut", 129 | desc = "add/remove file/directory to cut clipboard", 130 | }, 131 | { 132 | key = "c", 133 | action = "copy", 134 | desc = "add/remove file/directory to copy clipboard", 135 | }, 136 | { 137 | key = "p", 138 | action = "paste", 139 | desc = "paste from clipboard; cut clipboard has precedence over copy; will prompt for confirmation", 140 | }, 141 | { 142 | key = "y", 143 | action = "copy_name", 144 | desc = "copy name to system clipboard", 145 | }, 146 | { 147 | key = "Y", 148 | action = "copy_path", 149 | desc = "copy relative path to system clipboard", 150 | }, 151 | { 152 | key = "gy", 153 | action = "copy_absolute_path", 154 | desc = "copy absolute path to system clipboard", 155 | }, 156 | { 157 | key = "[e", 158 | action = "prev_diag_item", 159 | desc = "go to next diagnostic item", 160 | }, 161 | { 162 | key = "[c", 163 | action = "prev_git_item", 164 | desc = "go to next git item", 165 | }, 166 | { 167 | key = "]e", 168 | action = "next_diag_item", 169 | desc = "go to prev diagnostic item", 170 | }, 171 | { 172 | key = "]c", 173 | action = "next_git_item", 174 | desc = "go to prev git item", 175 | }, 176 | { 177 | key = "-", 178 | action = "dir_up", 179 | desc = "navigate up to the parent directory of the current file/directory", 180 | }, 181 | { 182 | key = "s", 183 | action = "system_open", 184 | desc = "open a file with default system application or a folder with default file manager, using |system_open| option", 185 | }, 186 | { 187 | key = "f", 188 | action = "live_filter", 189 | desc = "live filter nodes dynamically based on regex matching.", 190 | }, 191 | { 192 | key = "F", 193 | action = "clear_live_filter", 194 | desc = "clear live filter", 195 | }, 196 | { 197 | key = "q", 198 | action = "close", 199 | desc = "close tree window", 200 | }, 201 | { 202 | key = "W", 203 | action = "collapse_all", 204 | desc = "collapse the whole tree", 205 | }, 206 | { 207 | key = "E", 208 | action = "expand_all", 209 | desc = "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", 210 | }, 211 | { 212 | key = "S", 213 | action = "search_node", 214 | desc = "prompt the user to enter a path and then expands the tree to match the path", 215 | }, 216 | { 217 | key = ".", 218 | action = "run_file_command", 219 | desc = "enter vim command mode with the file the cursor is on", 220 | }, 221 | { 222 | key = "", 223 | action = "toggle_file_info", 224 | desc = "toggle a popup with file infos about the file under the cursor", 225 | }, 226 | { 227 | key = "g?", 228 | action = "toggle_help", 229 | desc = "toggle help", 230 | }, 231 | { 232 | key = "m", 233 | action = "toggle_mark", 234 | desc = "Toggle node in bookmarks", 235 | }, 236 | { 237 | key = "bmv", 238 | action = "bulk_move", 239 | desc = "Move all bookmarked nodes into specified location", 240 | }, 241 | } 242 | -- END_DEFAULT_MAPPINGS 243 | 244 | local M = { 245 | mappings = {}, 246 | custom_keypress_funcs = {}, 247 | } 248 | 249 | local function set_map_for(bufnr) 250 | local opts = { noremap = true, silent = true, nowait = true, buffer = bufnr } 251 | return function(mode, rhs) 252 | return function(lhs) 253 | vim.keymap.set(mode or "n", lhs, rhs, opts) 254 | end 255 | end 256 | end 257 | 258 | local function run_dispatch(action) 259 | return function() 260 | require("nvim-tree.actions.dispatch").dispatch(action) 261 | end 262 | end 263 | 264 | function M.apply_mappings(bufnr) 265 | local setter_for = set_map_for(bufnr) 266 | for _, b in pairs(M.mappings) do 267 | local rhs = b.cb or run_dispatch(b.action) 268 | if rhs then 269 | local setter = setter_for(b.mode, rhs) 270 | 271 | local keys = type(b.key) == "table" and b.key or { b.key } 272 | for _, key in pairs(keys) do 273 | setter(key) 274 | end 275 | end 276 | end 277 | end 278 | 279 | local function merge_mappings(user_mappings) 280 | if #user_mappings == 0 then 281 | return M.mappings 282 | end 283 | 284 | local function is_empty(s) 285 | return s == "" 286 | end 287 | 288 | local user_keys = {} 289 | local removed_keys = {} 290 | -- remove default mappings if action is a empty string 291 | for _, map in pairs(user_mappings) do 292 | if type(map.key) == "table" then 293 | for _, key in pairs(map.key) do 294 | table.insert(user_keys, key) 295 | if is_empty(map.action) then 296 | table.insert(removed_keys, key) 297 | end 298 | end 299 | else 300 | table.insert(user_keys, map.key) 301 | if is_empty(map.action) then 302 | table.insert(removed_keys, map.key) 303 | end 304 | end 305 | 306 | if map.action and type(map.action_cb) == "function" then 307 | if not is_empty(map.action) then 308 | M.custom_keypress_funcs[map.action] = map.action_cb 309 | else 310 | util.notify.warn "action can't be empty if action_cb provided" 311 | end 312 | end 313 | end 314 | 315 | local default_map = vim.tbl_filter(function(map) 316 | if type(map.key) == "table" then 317 | local filtered_keys = {} 318 | for _, key in pairs(map.key) do 319 | if not vim.tbl_contains(user_keys, key) and not vim.tbl_contains(removed_keys, key) then 320 | table.insert(filtered_keys, key) 321 | end 322 | end 323 | map.key = filtered_keys 324 | return not vim.tbl_isempty(map.key) 325 | else 326 | return not vim.tbl_contains(user_keys, map.key) and not vim.tbl_contains(removed_keys, map.key) 327 | end 328 | end, M.mappings) 329 | 330 | local user_map = vim.tbl_filter(function(map) 331 | return not is_empty(map.action) 332 | end, user_mappings) 333 | 334 | return vim.fn.extend(default_map, user_map) 335 | end 336 | 337 | local function copy_mappings(user_mappings) 338 | if #user_mappings == 0 then 339 | return M.mappings 340 | end 341 | 342 | for _, map in pairs(user_mappings) do 343 | if map.action and type(map.action_cb) == "function" then 344 | M.custom_keypress_funcs[map.action] = map.action_cb 345 | end 346 | end 347 | 348 | return user_mappings 349 | end 350 | 351 | local function cleanup_existing_mappings() 352 | local bufnr = view.get_bufnr() 353 | if bufnr == nil or not a.nvim_buf_is_valid(bufnr) then 354 | return 355 | end 356 | 357 | for _, b in pairs(M.mappings) do 358 | local keys = type(b.key) == "table" and b.key or { b.key } 359 | for _, key in pairs(keys) do 360 | vim.keymap.del(b.mode or "n", key, { buffer = bufnr }) 361 | end 362 | end 363 | end 364 | 365 | local function filter_mappings(mappings, keys) 366 | if type(keys) == "boolean" and keys then 367 | return {} 368 | elseif type(keys) == "table" then 369 | return vim.tbl_filter(function(m) 370 | if type(m.key) == "table" then 371 | m.key = vim.tbl_filter(function(k) 372 | return not vim.tbl_contains(keys, k) 373 | end, m.key) 374 | return #m.key > 0 375 | else 376 | return not vim.tbl_contains(keys, m.key) 377 | end 378 | end, vim.deepcopy(mappings)) 379 | else 380 | return vim.deepcopy(mappings) 381 | end 382 | end 383 | 384 | local DEFAULT_MAPPING_CONFIG = { 385 | custom_only = false, 386 | list = {}, 387 | } 388 | 389 | function M.setup(opts) 390 | require("nvim-tree.actions.fs.trash").setup(opts) 391 | require("nvim-tree.actions.node.system-open").setup(opts) 392 | require("nvim-tree.actions.node.file-popup").setup(opts) 393 | require("nvim-tree.actions.node.open-file").setup(opts) 394 | require("nvim-tree.actions.root.change-dir").setup(opts) 395 | require("nvim-tree.actions.fs.create-file").setup(opts) 396 | require("nvim-tree.actions.fs.rename-file").setup(opts) 397 | require("nvim-tree.actions.fs.remove-file").setup(opts) 398 | require("nvim-tree.actions.fs.copy-paste").setup(opts) 399 | require("nvim-tree.actions.tree-modifiers.expand-all").setup(opts) 400 | 401 | cleanup_existing_mappings() 402 | 403 | M.mappings = filter_mappings(DEFAULT_MAPPINGS, opts.remove_keymaps) 404 | 405 | local user_map_config = (opts.view or {}).mappings or {} 406 | local options = vim.tbl_deep_extend("force", DEFAULT_MAPPING_CONFIG, user_map_config) 407 | if options.custom_only then 408 | M.mappings = copy_mappings(options.list) 409 | else 410 | M.mappings = merge_mappings(options.list) 411 | end 412 | 413 | require("nvim-tree.actions.dispatch").setup(M.custom_keypress_funcs) 414 | 415 | log.line("config", "active mappings") 416 | log.raw("config", "%s\n", vim.inspect(M.mappings)) 417 | end 418 | 419 | return M 420 | --------------------------------------------------------------------------------