Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions doc/nvim-tree-lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ Show the mappings: `g?`
`Y` Copy Relative Path |nvim_tree.api.fs.copy.relative_path()|
`<2-LeftMouse>` Open |nvim_tree.api.node.open.edit()|
`<2-RightMouse>` CD |nvim_tree.api.tree.change_root_to_node()|

`m` Toggle Bookmark |nvim_tree.api.marks.toggle_visual()|
`d` Delete |nvim_tree.api.fs.remove_visual()|
`D` Trash |nvim_tree.api.fs.trash_visual()|
`c` Copy |nvim_tree.api.fs.copy.visual()|
`x` Cut |nvim_tree.api.fs.cut_visual()|

==============================================================================
Quickstart: Custom Mappings *nvim-tree-quickstart-custom-mappings*
Expand Down Expand Up @@ -458,6 +464,12 @@ You are encouraged to copy these to your {on_attach} function. >lua
vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path"))
vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, opts("Open"))
vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, opts("CD"))

vim.keymap.set("x", "m", api.marks.toggle_visual, opts("Toggle Bookmark"))
vim.keymap.set("x", "d", api.fs.remove_visual, opts("Delete"))
vim.keymap.set("x", "D", api.fs.trash_visual, opts("Trash"))
vim.keymap.set("x", "c", api.fs.copy.visual, opts("Copy"))
vim.keymap.set("x", "x", api.fs.cut_visual, opts("Cut"))
-- END_ON_ATTACH_DEFAULT
<
Alternatively, you may apply these default mappings from your
Expand Down Expand Up @@ -2461,6 +2473,9 @@ copy.relative_path({node}) *nvim_tree.api.fs.copy.relative_path()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`)

copy.visual() *nvim_tree.api.fs.copy.visual()*
Copy all visually selected nodes to the nvim-tree clipboard.

create({node}) *nvim_tree.api.fs.create()*
Prompt to create a file or directory.

Expand All @@ -2479,6 +2494,9 @@ cut({node}) *nvim_tree.api.fs.cut()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`)

cut_visual() *nvim_tree.api.fs.cut_visual()*
Cut all visually selected nodes to the nvim-tree clipboard.

paste({node}) *nvim_tree.api.fs.paste()*
Paste from the nvim-tree clipboard.

Expand All @@ -2496,6 +2514,9 @@ remove({node}) *nvim_tree.api.fs.remove()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`)

remove_visual() *nvim_tree.api.fs.remove_visual()*
Delete all visually selected nodes, prompting once.

rename({node}) *nvim_tree.api.fs.rename()*
Prompt to rename by name.

Expand Down Expand Up @@ -2532,6 +2553,9 @@ trash({node}) *nvim_tree.api.fs.trash()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`)

trash_visual() *nvim_tree.api.fs.trash_visual()*
Trash all visually selected nodes, prompting once.


==============================================================================
API: git *nvim-tree-api-git*
Expand Down Expand Up @@ -2609,6 +2633,9 @@ toggle({node}) *nvim_tree.api.marks.toggle()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`) file or directory

toggle_visual() *nvim_tree.api.marks.toggle_visual()*
Toggle mark on all visually selected nodes.


==============================================================================
API: node *nvim-tree-api-node*
Expand Down
20 changes: 20 additions & 0 deletions lua/nvim-tree/_meta/api/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ function nvim_tree.api.fs.copy.filename(node) end
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.copy.node(node) end

---
---Copy all visually selected nodes to the nvim-tree clipboard.
---
function nvim_tree.api.fs.copy.visual() end

---
---Copy the path relative to the tree root to the system clipboard.
---
Expand All @@ -56,6 +61,11 @@ function nvim_tree.api.fs.create(node) end
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.cut(node) end

---
---Cut all visually selected nodes to the nvim-tree clipboard.
---
function nvim_tree.api.fs.cut_visual() end

---
---Paste from the nvim-tree clipboard.
---
Expand All @@ -75,6 +85,11 @@ function nvim_tree.api.fs.print_clipboard() end
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.remove(node) end

---
---Delete all visually selected nodes, prompting once.
---
function nvim_tree.api.fs.remove_visual() end

---
---Prompt to rename by name.
---
Expand Down Expand Up @@ -111,4 +126,9 @@ function nvim_tree.api.fs.rename_sub(node) end
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.trash(node) end

---
---Trash all visually selected nodes, prompting once.
---
function nvim_tree.api.fs.trash_visual() end

return nvim_tree.api.fs
5 changes: 5 additions & 0 deletions lua/nvim-tree/_meta/api/marks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ function nvim_tree.api.marks.list() end
---@param node? nvim_tree.api.Node file or directory
function nvim_tree.api.marks.toggle(node) end

---
---Toggle mark on all visually selected nodes.
---
function nvim_tree.api.marks.toggle_visual() end

---
---Clear all marks.
---
Expand Down
47 changes: 47 additions & 0 deletions lua/nvim-tree/api/impl/post.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,47 @@ local function wrap_explorer_member(explorer_member, member_method)
end
end

---Exit visual mode synchronously.
local function exit_visual_mode()
local esc = vim.api.nvim_replace_termcodes("<Esc>", true, false, true)
vim.api.nvim_feedkeys(esc, "nx", false)
end

---Wrap a single-node function to operate on visual selection range.
---@param fn fun(node: Node): any
---@return fun(): any
local function wrap_visual_range(fn)
return function()
local explorer = require("nvim-tree.core").get_explorer()
if not explorer then return end
local start_line = vim.fn.line("v")
local end_line = vim.fn.line(".")
if start_line > end_line then start_line, end_line = end_line, start_line end
local nodes = explorer:get_nodes_in_range(start_line, end_line)
exit_visual_mode()
for _, node in ipairs(nodes) do
fn(node)
end
end
end

---Wrap a bulk operation that collects visual nodes and passes them all at once.
---@param member string explorer member name
---@param method string method name to invoke on member
---@return fun(): any
local function wrap_visual_bulk(member, method)
return function()
local explorer = require("nvim-tree.core").get_explorer()
if not explorer then return end
local start_line = vim.fn.line("v")
local end_line = vim.fn.line(".")
if start_line > end_line then start_line, end_line = end_line, start_line end
local nodes = explorer:get_nodes_in_range(start_line, end_line)
exit_visual_mode()
explorer[member][method](explorer[member], nodes)
end
end

---@class NodeEditOpts
---@field quit_on_open boolean|nil default false
---@field focus boolean|nil default true
Expand Down Expand Up @@ -254,6 +295,12 @@ function M.hydrate(api)
api.marks.navigate.next = wrap_explorer_member("marks", "navigate_next")
api.marks.navigate.prev = wrap_explorer_member("marks", "navigate_prev")
api.marks.navigate.select = wrap_explorer_member("marks", "navigate_select")
api.marks.toggle_visual = wrap_visual_range(wrap_explorer_member("marks", "toggle"))

api.fs.copy.visual = wrap_visual_range(wrap_explorer_member("clipboard", "copy"))
api.fs.cut_visual = wrap_visual_range(wrap_explorer_member("clipboard", "cut"))
api.fs.remove_visual = wrap_visual_bulk("marks", "bulk_delete_nodes")
api.fs.trash_visual = wrap_visual_bulk("marks", "bulk_trash_nodes")

api.map.keymap.current = keymap.get_keymap

Expand Down
16 changes: 16 additions & 0 deletions lua/nvim-tree/explorer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,22 @@ function Explorer:find_node(fn)
return node, i
end

---Get all nodes in a line range (inclusive), for visual selection operations.
---@param start_line integer
---@param end_line integer
---@return Node[]
function Explorer:get_nodes_in_range(start_line, end_line)
local nodes_by_line = self:get_nodes_by_line(core.get_nodes_starting_line())
local nodes = {}
for line = start_line, end_line do
local node = nodes_by_line[line]
if node and node.absolute_path then
table.insert(nodes, node)
end
end
return nodes
end

--- Return visible nodes indexed by line
---@param line_start number
---@return table
Expand Down
8 changes: 6 additions & 2 deletions lua/nvim-tree/help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,13 @@ local function compute(map)
local head_rhs1 = "exit: q"
local head_rhs2 = string.format("sort by %s: s", M.config.sort_by == "key" and "description" or "keymap")

-- formatted lhs and desc from active keymap
-- formatted lhs and desc from active keymap, prefixing visual mode keys
local mappings = vim.tbl_map(function(m)
return { lhs = tidy_lhs(m.lhs), desc = tidy_desc(m.desc) }
local lhs = tidy_lhs(m.lhs)
if m.mode == "x" then
lhs = "[v] " .. lhs
end
return { lhs = lhs, desc = tidy_desc(m.desc) }
end, map)

-- sorter function for mappings
Expand Down
6 changes: 6 additions & 0 deletions lua/nvim-tree/keymap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ function M.on_attach_default(bufnr)
vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path"))
vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, opts("Open"))
vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, opts("CD"))

vim.keymap.set("x", "m", api.marks.toggle_visual, opts("Toggle Bookmark"))
vim.keymap.set("x", "d", api.fs.remove_visual, opts("Delete"))
vim.keymap.set("x", "D", api.fs.trash_visual, opts("Trash"))
vim.keymap.set("x", "c", api.fs.copy.visual, opts("Copy"))
vim.keymap.set("x", "x", api.fs.cut_visual, opts("Cut"))
-- END_ON_ATTACH_DEFAULT
end

Expand Down
133 changes: 133 additions & 0 deletions lua/nvim-tree/marks/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,139 @@ function Marks:bulk_trash()
end
end

---Filter out nodes that are descendants of other nodes in the list.
---When a directory is selected along with its children, only the directory needs to be operated on.
---@private
---@param nodes Node[]
---@return Node[]
function Marks:filter_descendant_nodes(nodes)
local dominated = {}
for i, a in ipairs(nodes) do
for j, b in ipairs(nodes) do
if i ~= j then
local prefix = b.absolute_path .. utils.path_separator
if a.absolute_path:sub(1, #prefix) == prefix then
dominated[i] = true
break
end
end
end
end
local filtered = {}
for i, node in ipairs(nodes) do
if not dominated[i] then
table.insert(filtered, node)
end
end
return filtered
end

---Delete a list of nodes with a single prompt; used for visual selection operations.
---@public
---@param nodes Node[]
function Marks:bulk_delete_nodes(nodes)
-- Filter out parent directory entries ("..") to avoid deleting the parent directory.
local filtered_nodes = {}
for _, node in ipairs(nodes) do
if node.name ~= ".." then
table.insert(filtered_nodes, node)
end
end

if #filtered_nodes == 0 then
return
end

nodes = self:filter_descendant_nodes(filtered_nodes)

local function execute()
for i = #nodes, 1, -1 do
remove_file.remove(nodes[i])
end
if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer()
end
end

if self.explorer.opts.ui.confirm.remove then
local default_yes = self.explorer.opts.ui.confirm.default_yes == true
local prompt_select = string.format("Remove %d selected ?", #nodes)
local prompt_input, items_short, items_long

if default_yes then
prompt_input = prompt_select .. " Y/n: "
items_short = { "", "n" }
items_long = { "Yes", "No" }
else
prompt_input = prompt_select .. " y/N: "
items_short = { "", "y" }
items_long = { "No", "Yes" }
end

lib.prompt(prompt_input, prompt_select, items_short, items_long, "nvimtree_visual_delete", function(item_short)
utils.clear_prompt()
if item_short == "y" or (default_yes and item_short ~= "n") then
execute()
end
end)
else
execute()
end
end

---Trash a list of nodes with a single prompt; used for visual selection operations.
---@public
---@param nodes Node[]
function Marks:bulk_trash_nodes(nodes)
-- Filter out parent directory entries ("..") to avoid trashing the parent directory.
local filtered_nodes = {}
for _, node in ipairs(nodes) do
if node.name ~= ".." then
table.insert(filtered_nodes, node)
end
end

if #filtered_nodes == 0 then
return
end

nodes = self:filter_descendant_nodes(filtered_nodes)

local function execute()
for i = #nodes, 1, -1 do
trash.remove(nodes[i])
end
if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer()
end
end

if self.explorer.opts.ui.confirm.trash then
local default_yes = self.explorer.opts.ui.confirm.default_yes == true
local prompt_select = string.format("Trash %d selected ?", #nodes)
local prompt_input, items_short, items_long

if default_yes then
prompt_input = prompt_select .. " Y/n: "
items_short = { "", "n" }
items_long = { "Yes", "No" }
else
prompt_input = prompt_select .. " y/N: "
items_short = { "", "y" }
items_long = { "No", "Yes" }
end

lib.prompt(prompt_input, prompt_select, items_short, items_long, "nvimtree_visual_trash", function(item_short)
utils.clear_prompt()
if item_short == "y" or (default_yes and item_short ~= "n") then
execute()
end
end)
else
execute()
end
end

---Move marked
---@public
function Marks:bulk_move()
Expand Down
2 changes: 1 addition & 1 deletion scripts/help-defaults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/ON_ATTACH_DEFAULT.lua

# help human
echo > /tmp/ON_ATTACH_DEFAULT.help
sed -E "s/^ *vim.keymap.set\(\"n\", \"(.*)\",.*api(.*),.*opts\(\"(.*)\".*$/'\`\1\`' '\3' '|nvim_tree.api\2()|'/g
sed -E "s/^ *vim.keymap.set\(\".\", \"(.*)\",.*api(.*),.*opts\(\"(.*)\".*$/'\`\1\`' '\3' '|nvim_tree.api\2()|'/g
" /tmp/ON_ATTACH_DEFAULT.lua | while read -r line
do
eval "printf '%-17.17s %-26.26s %s\n' ${line}" >> /tmp/ON_ATTACH_DEFAULT.help
Expand Down
Loading