Skip to content
Michael Härtl edited this page Mar 26, 2023 · 85 revisions

h, j, k, l Style Navigation And Editing

@eddiebergman

Hotkeys that keep your fingers using h, j, k, l for navigation and opening.

  • Ctrl + h | Open tree
  • h | Collapse current containing folder
  • H | Collapse Tree
  • l | Open node if it is a folder, else edit the file and close tree
  • L | Open node if it is a folder, else create vsplit of file and keep cursor focus on tree

In particular, L is quite nice for opening a few files in quick succession without losing focus of the tree while using l for just editing a file in the current buffer.

To do so, you'll want to add some mappings with your own action_cb:

local config = {
    view = {
        mappings = {
            custom_only = false,
            list = {
                { key = "l", action = "edit", action_cb = edit_or_open },
                { key = "L", action = "vsplit_preview", action_cb = vsplit_preview },
                { key = "h", action = "close_node" },
                { key = "H", action = "collapse_all", action_cb = collapse_all }
            }
        },
    },
    actions = {
        open_file = {
            quit_on_open = false
        }
    }
}

vim.api.nvim_set_keymap("n", "<C-h>", ":NvimTreeToggle<cr>" ,{silent = true, noremap = true})
require('nvim-tree').setup(config)

You'll also need to define these callbacks that nvim-tree will use, they'll need to be defined above the snippet above:

local lib = require("nvim-tree.lib")
local view = require("nvim-tree.view")


local function collapse_all()
    require("nvim-tree.actions.tree-modifiers.collapse-all").fn()
end

local function edit_or_open()
    -- open as vsplit on current node
    local action = "edit"
    local node = lib.get_node_at_cursor()

    -- Just copy what's done normally with vsplit
    if node.link_to and not node.nodes then
        require('nvim-tree.actions.node.open-file').fn(action, node.link_to)
        view.close() -- Close the tree if file was opened

    elseif node.nodes ~= nil then
        lib.expand_or_collapse(node)

    else
        require('nvim-tree.actions.node.open-file').fn(action, node.absolute_path)
        view.close() -- Close the tree if file was opened
    end

end

local function vsplit_preview()
    -- open as vsplit on current node
    local action = "vsplit"
    local node = lib.get_node_at_cursor()

    -- Just copy what's done normally with vsplit
    if node.link_to and not node.nodes then
        require('nvim-tree.actions.node.open-file').fn(action, node.link_to)

    elseif node.nodes ~= nil then
        lib.expand_or_collapse(node)

    else
        require('nvim-tree.actions.node.open-file').fn(action, node.absolute_path)

    end

    -- Finally refocus on tree if it was lost
    view.focus()
end

Git Stage Unstage Files And Directories From The Tree

@Hubro

You can stage and unstage files directly in the tree view with this config:

local lib = require("nvim-tree.lib")

local git_add = function()
  local node = lib.get_node_at_cursor()
  local gs = node.git_status.file

  -- If the file is untracked, unstaged or partially staged, we stage it
  if gs == "??" or gs == "MM" or gs == "AM" or gs == " M" then
    vim.cmd("silent !git add " .. node.absolute_path)

  -- If the file is staged, we unstage
  elseif gs == "M " or gs == "A " then
    vim.cmd("silent !git restore --staged " .. node.absolute_path)
  end

  lib.refresh_tree()
end

require("nvim-tree").setup {
  view = {
    mappings = {
      list = {
        { key = "ga", action = "git_add", action_cb = git_add },
      }
    },
  }
}

If you target a file or directory in the tree and press ga, it will be git staged. If it's already staged, it will instead be unstaged.

Find File From Node In Telescope

@kyazdani42

Here is a small snippet of code to open the file under the cursor in telescope

File named 'treeutils.lua'

local lib = require'nvim-tree.lib'
local openfile = require'nvim-tree.actions.node.open-file'
local actions = require'telescope.actions'
local action_state = require'telescope.actions.state'
local M = {}

local view_selection = function(prompt_bufnr, map)
  actions.select_default:replace(function()
    actions.close(prompt_bufnr)
    local selection = action_state.get_selected_entry()
    local filename = selection.filename
    if (filename == nil) then
      filename = selection[1]
    end
    openfile.fn('preview', filename)
  end)
  return true
end

function M.launch_live_grep(opts)
  return M.launch_telescope("live_grep", opts)
end

function M.launch_find_files(opts)
  return M.launch_telescope("find_files", opts)
end

function M.launch_telescope(func_name, opts)
  local telescope_status_ok, _ = pcall(require, "telescope")
  if not telescope_status_ok then
    return
  end
  local lib_status_ok, lib = pcall(require, "nvim-tree.lib")
  if not lib_status_ok then
    return
  end
  local node = lib.get_node_at_cursor()
  local is_folder = node.fs_stat and node.fs_stat.type == 'directory' or false
  local basedir = is_folder and node.absolute_path or vim.fn.fnamemodify(node.absolute_path, ":h")
  if (node.name == '..' and TreeExplorer ~= nil) then
    basedir = TreeExplorer.cwd
  end
  opts = opts or {}
  opts.cwd = basedir
  opts.search_dirs = { basedir }
  opts.attach_mappings = view_selection
  return require("telescope.builtin")[func_name](opts)
end

return M

And then to use it, add something like this depending on your configuration structure

local function custom_callback(callback_name)
    return string.format(":lua require('treeutils').%s()<CR>", callback_name)
end

-- this is passed in the call to setup

require 'nvim-tree'.setup {
    view = {
        mappings = {
            list = {
              { key = "<c-f>", cb = custom_callback "launch_find_files" },
              { key = "<c-g>", cb = custom_callback "launch_live_grep" },
            }
        }
    }
}

Filter Directories With Live Filter

@kay-adamof

By default, the live filter doesn't filter the directory names but it's so painful if you have many folders in your project.

Put this code to your init.lua.

require("nvim-tree").setup {
  live_filter = {
    prefix = "[FILTER]: ",
    always_show_folders = false, -- Turn into false from true by default
  }
}

Fix tab titles when opening file in new tab

@sarahkittyy

By default, opening a file in a new tab leaves NvimTree_1 as the text in the tab bar. This function will run <C-w>l before opening a new tab to set the tab title to the split adjacent to the tree.

local swap_then_open_tab = function()
	local node = require('nvim-tree.lib').get_node_at_cursor()
	vim.cmd("wincmd l")
	require('nvim-tree.api').node.open.tab(node)
end
-- { key = "t", action = "swap_then_open_tab", action_cb = swap_then_open_tab },

Center a floating nvim-tree window

For nvim-tree to be a centered floating window, add this to your config:

local HEIGHT_RATIO = 0.8  -- You can change this
local WIDTH_RATIO = 0.5   -- You can change this too

require('nvim-tree').setup({
  view = {
    float = {
      enable = true,
      open_win_config = function()
        local screen_w = vim.opt.columns:get()
        local screen_h = vim.opt.lines:get() - vim.opt.cmdheight:get()
        local window_w = screen_w * WIDTH_RATIO
        local window_h = screen_h * HEIGHT_RATIO
        local window_w_int = math.floor(window_w)
        local window_h_int = math.floor(window_h)
        local center_x = (screen_w - window_w) / 2
        local center_y = ((vim.opt.lines:get() - window_h) / 2)
                         - vim.opt.cmdheight:get()
        return {
          border = 'rounded',
          relative = 'editor',
          row = center_y,
          col = center_x,
          width = window_w_int,
          height = window_h_int,
        }
        end,
    },
    width = function()
      return math.floor(vim.opt.columns:get() * WIDTH_RATIO)
    end,
  },
})

screen

Original solution by @davidsierradz and @alex-courtis (see #1538). Recipe written by @Kryzar.

Creating an actions menu using Telescope

@Tolomeo

It is possible to use Telescope as a menu, listing custom items and executing a callback on item selection. Following is a minimal example.

First of all, you will need a list of labels to show in the menu, along with their associated callbacks. Note that nvim-tree actions are directly requirable from nvim-tree.api.

local tree_actions = {
	{
		name = "Create node",
		handler = require("nvim-tree.api").fs.create,
	},
	{
		name = "Remove node",
		handler = require("nvim-tree.api").fs.remove,
	},
	{
		name = "Trash node",
		handler = require("nvim-tree.api").fs.trash,
	},
	{
		name = "Rename node",
		handler = require("nvim-tree.api").fs.rename,
	},
	{
		name = "Fully rename node",
		handler = require("nvim-tree.api").fs.rename_sub,
	},
	{
		name = "Copy",
		handler = require("nvim-tree.api").fs.copy.node,
	},

	-- ... other custom actions you may want to display in the menu
}

You will then need a function used to display the actions in a Telescope picker. The function will be keymapped inside on_attach nvim-tree config option and as such it will receive the highlighted node.

local function tree_actions_menu(node)
	local entry_maker = function(menu_item)
		return {
			value = menu_item,
			ordinal = menu_item.name,
			display = menu_item.name,
		}
	end

	local finder = require("telescope.finders").new_table({
		results = tree_actions,
		entry_maker = entry_maker,
	})

	local sorter = require("telescope.sorters").get_generic_fuzzy_sorter()

	local default_options = {
		finder = finder,
		sorter = sorter,
		attach_mappings = function(prompt_buffer_number)
			local actions = require("telescope.actions")

			-- On item select
			actions.select_default:replace(function()
				local state = require("telescope.actions.state")
				local selection = state.get_selected_entry()
				-- Closing the picker
				actions.close(prompt_buffer_number)
				-- Executing the callback
				selection.value.handler(node)
			end)

			-- The following actions are disabled in this example
			-- You may want to map them too depending on your needs though
			actions.add_selection:replace(function() end)
			actions.remove_selection:replace(function() end)
			actions.toggle_selection:replace(function() end)
			actions.select_all:replace(function() end)
			actions.drop_all:replace(function() end)
			actions.toggle_all:replace(function() end)

			return true
		end,
	}

	-- Opening the menu
	require("telescope.pickers").new({ prompt_title = "Tree menu" }, default_options):find()
end

Finally, you will need to add a keymap for the created function:

view.mapping:

view = {
  mappings = {
    list = {
      { key = "<C-Space>", action = "tree actions", action_cb = tree_actions_menu }
      ---

or

on_attach:

on_attach = function(buffer)
  vim.keymap.set("n", "<C-Space>", tree_actions_menu, { buffer = buffer, noremap = true, silent = true })
  ---

Change Root To Global Current Working Directory

@alex-courtis

When not using update_focused_file.update_root the tree's current working directory may differ from the global.

You can manually change the root to the global cwd:

  local function change_root_to_global_cwd()
    local api = require("nvim-tree.api")
    local global_cwd = vim.fn.getcwd(-1, -1)
    api.tree.change_root(global_cwd)
  end
---
      mappings = {
        list = {
          { key = "<C-C>", action = "global_cwd", action_cb = change_root_to_global_cwd },
---

Silently open a new tab

@TroySigx

Sometimes, we only want to open a tab, but don't want to jump to that tab immediately.

local function open_tab_silent(node)
  local api = require("nvim-tree.api")

  api.node.open.tab(node)
  vim.cmd.tabprev()

end
---
require("nvim-tree").setup({
  disable_netrw = false,
  view = {
    mappings = {
      list = {
        { key = "T", action = "open_tab_silent", action_cb = open_tab_silent },
      },
    }
  }
})
---

Sorting files naturally (respecting numbers within files names)

@zer0-x

The current available sorting by name algorithm just sorts files names by comparing every character in them. This leads to a strange and undesirable arrangement when you have numbered files names, since it will sort numbers as any normal letter.

For example files will be sorted as 1 foo, 20 foo, 3 foo rather then 1 foo, 3 foo, 20 foo. The code bellow should solve that.

This is it, the fixed and improved version:

local function natural_cmp(left, right)
	left = left.name:lower()
	right = right.name:lower()

	if left == right then
		return false
	end

	for i = 1, math.max(string.len(left), string.len(right)), 1 do
		local l = string.sub(left, i, -1)
		local r = string.sub(right, i, -1)

		if type(tonumber(string.sub(l, 1, 1))) == "number" and type(tonumber(string.sub(r, 1, 1))) == "number" then
			local l_number = tonumber(string.match(l, "^[0-9]+"))
			local r_number = tonumber(string.match(r, "^[0-9]+"))

			if l_number ~= r_number then
				return l_number < r_number
			end
		elseif string.sub(l, 1, 1) ~= string.sub(r, 1, 1) then
			return l < r
		end
	end
end

require("nvim-tree").setup({
	sort_by = function(nodes)
		table.sort(nodes, natural_cmp)
	end,
})

Related discussion: #1896

References: lifthrasiir/rust-natord/lib.rs#L142, lifthrasiir/rust-natord/lib.rs#L37

Automatically open file upon creation

@magoz

local api = require("nvim-tree.api")
api.events.subscribe(api.events.Event.FileCreated, function(file)
  vim.cmd("edit " .. file.fname)
end)

Vinegar Style

nvim-tree can behave like vinegar. To allow this, you will need to configure it in a specific way:

Use require"nvim-tree".open_replacing_current_buffer() instead of the default open command. You can easily implement a toggle using this too:

local function toggle_replace()
  local view = require"nvim-tree.view"
  local api = require"nvim-tree.api"
  if view.is_visible() then
    api.tree.close()
  else
    require"nvim-tree".open_replacing_current_buffer()
  end
end

Use the edit_in_place action to edit files. It's bound to <C-e> by default, vinegar uses <CR>. You can override this with:

require"nvim-tree".setup {
  view = {
    mappings = {
      list = {
        { key = "<CR>", action = "edit_in_place" }
      }
    }
  }
}

Going up a dir is bound to - by default in nvim-tree which is identical to vinegar, no change is needed here.

You'll also need to set nvim-tree.hijack_netrw to true during setup. A good functionality to enable is nvim-tree.hijack_directories

NERDTree Style Copy File To

@mangelozzi nvim-tree config

The NERDTree way of copying files works like this:

  1. Select the node in the tree
  2. Activate copy file to
  3. Edit the path to be the destination path of the copied file, you can use file completion to to aid entering in the new path.
  4. It copies the file to the new path, and creates any parent dirs as required.

Below the copy_file_to function is bound to c, if you wish to use it in additional to the existing c function, you will have to map it to some other hotkey

local function copy_file_to(node)
    local file_src = node['absolute_path']
    -- The args of input are {prompt}, {default}, {completion}
    -- Read in the new file path using the existing file's path as the baseline.
    local file_out = vim.fn.input("COPY TO: ", file_src, "file")
    -- Create any parent dirs as required
    local dir = vim.fn.fnamemodify(file_out, ":h")
    vim.fn.system { 'mkdir', '-p', dir }
    -- Copy the file
    vim.fn.system { 'cp', '-R', file_src, file_out }
end

require('nvim-tree').setup {
    view = {
        mappings = {
            list = {
                { key = "c", action = "copy_file_to", action_cb = copy_file_to },
            }
        }
    }
}

Show dynamic actions popup

@JoseConseco

Show dynamic Hydra popup with file actions that user can perform on files, when NvimTree window is focused. Requires hydra.nvim - this plugin can draw menu popups and execute custom, user defined, commands. nvim-tree_hydra.webm

-- auto show hydra on nvimtree focus
local function change_root_to_global_cwd()
  local global_cwd = vim.fn.getcwd()
  -- local global_cwd = vim.fn.getcwd(-1, -1)
  api.tree.change_root(global_cwd)
end

local hint = [[
 _w_: cd CWD   _c_: Path yank           _/_: Filter
 _y_: Copy     _x_: Cut                 _p_: Paste
 _r_: Rename   _d_: Remove              _n_: New
 _h_: Hidden   _?_: Help
 ^
]]
-- ^ ^           _q_: exit

local nvim_tree_hydra = nil
local nt_au_group = vim.api.nvim_create_augroup("NvimTreeHydraAu", { clear = true })

local Hydra = require "hydra"
local function spawn_nvim_tree_hydra()
  nvim_tree_hydra = Hydra {
          name = "NvimTree",
          hint = hint,
          config = {
              color = "pink",
              invoke_on_body = true,
              buffer = 0, -- only for active buffer
              hint = {
                  position = "bottom",
                  border = "rounded",
              },
          },
          mode = "n",
          body = "H",
          heads = {
              { "w", change_root_to_global_cwd,     { silent = true } },
              { "c", api.fs.copy.absolute_path,     { silent = true } },
              { "/", api.live_filter.start,         { silent = true } },
              { "y", api.fs.copy.node,              { silent = true } },
              { "x", api.fs.cut,                    { exit = true, silent = true } },
              { "p", api.fs.paste,                  { exit = true, silent = true } },
              { "r", api.fs.rename,                 { silent = true } },
              { "d", api.fs.remove,                 { silent = true } },
              { "n", api.fs.create,                 { silent = true } },
              { "h", api.tree.toggle_hidden_filter, { silent = true } },
              { "?", api.tree.toggle_help,          { silent = true } },
              -- { "q", nil, { exit = true, nowait = true } },
          },
      }
  nvim_tree_hydra:activate()
end

vim.api.nvim_create_autocmd({ "BufEnter" }, {
    pattern = "*",
    callback = function(opts)
      if vim.bo[opts.buf].filetype == "NvimTree" then
        spawn_nvim_tree_hydra()
      else
        if nvim_tree_hydra then
          nvim_tree_hydra:exit()
        end
      end
    end,
    group = nt_au_group,
})      

Workaround when using rmagatti/auto-session

@nikolasmish

There is currently an issue with restoring nvim-tree fully when using rmagatti/auto-session. Upon restoring the session, nvim-tree buffer will be empty, sometimes positioned in strange places with random dimensions. This issue only happens when saving session with nvim-tree open. To prevent this from happening you can use the following autocmd:

vim.api.nvim_create_autocmd({ 'BufEnter' }, {
  pattern = 'NvimTree*',
  callback = function()
    local view = require('nvim-tree.view')
    local is_visible = view.is_visible()

    local api = require('nvim-tree.api')
    if not is_visible then
      api.tree.open()
    end
  end,
})

This autocmd will check whether nvim-tree is open when the session is restored and refresh it.

Go to last used hidden buffer when deleting a buffer

@mikehaertl

Withouth nvim-tree when a buffer in a single window is deleted with :bdelete neovim goes to the most recent entry in the jump list that points into a loaded buffer (i.e. some hidden buffer). When nvim-tree is open you end up with a single window with nvim-tree instead. The following script tries to fix this.

vim.api.nvim_create_autocmd("BufEnter", {
  nested = true,
  callback = function()
    -- Only 1 window with nvim-tree left: we probably closed a file buffer
    if #vim.api.nvim_list_wins() == 1 and require("nvim-tree.utils").is_nvim_tree_buf() then
      local api = require('nvim-tree.api')
      -- Required to let the close event complete. An error is thrown without this.
      vim.defer_fn(function()
        -- close nvim-tree: will go to the last hidden buffer used before closing
        api.tree.toggle({find_file = true, focus = true})
        -- re-open nivm-tree
        api.tree.toggle({find_file = true, focus = true})
        -- nvim-tree is still the active window. Go to the previous window.
        vim.cmd("wincmd p")
      end, 0)
    end
  end
})
Clone this wiki locally