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
254 changes: 131 additions & 123 deletions doc/nvim-tree-lua.txt

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion lua/nvim-tree/_meta/api/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function nvim_tree.api.fs.copy.filename(node) end

---
---Copy to the nvim-tree clipboard.
---In visual mode, copies all nodes in the visual selection.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.copy.node(node) end
Expand All @@ -52,6 +53,7 @@ function nvim_tree.api.fs.create(node) end

---
---Cut to the nvim-tree clipboard.
---In visual mode, cuts all nodes in the visual selection.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.cut(node) end
Expand All @@ -71,6 +73,7 @@ function nvim_tree.api.fs.print_clipboard() end

---
---Delete from the file system.
---In visual mode, deletes all nodes in the visual selection with a single prompt.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.remove(node) end
Expand Down Expand Up @@ -106,7 +109,8 @@ function nvim_tree.api.fs.rename_node(node) end
function nvim_tree.api.fs.rename_sub(node) end

---
---Trash as per |nvim_tree.config.trash|
---Trash as per |nvim_tree.config.trash|.
---In visual mode, trashes all nodes in the visual selection with a single prompt.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.trash(node) end
Expand Down
2 changes: 1 addition & 1 deletion lua/nvim-tree/_meta/api/marks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function nvim_tree.api.marks.get() end
function nvim_tree.api.marks.list() end

---
---Toggle mark.
---Toggle mark. In visual mode, toggles all nodes in the visual selection.
---
---@param node? nvim_tree.api.Node file or directory
function nvim_tree.api.marks.toggle(node) end
Expand Down
207 changes: 135 additions & 72 deletions lua/nvim-tree/actions/fs/clipboard.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local find_file = require("nvim-tree.actions.finders.find-file").fn

local Class = require("nvim-tree.classic")
local DirectoryNode = require("nvim-tree.node.directory")
local Node = require("nvim-tree.node")

---@alias ClipboardAction "copy" | "cut"
---@alias ClipboardData table<ClipboardAction, Node[]>
Expand Down Expand Up @@ -108,67 +109,18 @@ local function do_copy(source, destination)
return true
end

---Paste a single item with no conflict handling.
---@param source string
---@param dest string
---@param action ClipboardAction
---@param action_fn ClipboardActionFn
---@return boolean|nil -- success
---@return string|nil -- error message
local function do_single_paste(source, dest, action, action_fn)
local notify_source = notify.render_path(source)

log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest)

local dest_stats, err, err_name = vim.loop.fs_stat(dest)
if not dest_stats and err_name ~= "ENOENT" then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (err or "???"))
return false, err
end

local function on_process()
local success, error = action_fn(source, dest)
if not success then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???"))
return false, error
end

find_file(utils.path_remove_trailing(dest))
end

if dest_stats then
local input_opts = {
prompt = "Rename to ",
default = dest,
completion = "dir",
}

if source == dest then
vim.ui.input(input_opts, function(new_dest)
utils.clear_prompt()
if new_dest then
do_single_paste(source, new_dest, action, action_fn)
end
end)
else
local prompt_select = "Overwrite " .. dest .. " ?"
local prompt_input = prompt_select .. " R(ename)/y/n: "
lib.prompt(prompt_input, prompt_select, { "", "y", "n" }, { "Rename", "Yes", "No" }, "nvimtree_overwrite_rename", function(item_short)
utils.clear_prompt()
if item_short == "y" then
on_process()
elseif item_short == "" or item_short == "r" then
vim.ui.input(input_opts, function(new_dest)
utils.clear_prompt()
if new_dest then
do_single_paste(source, new_dest, action, action_fn)
end
end)
end
end)
end
else
on_process()
local function do_paste_one(source, dest, action, action_fn)
log.line("copy_paste", "do_paste_one '%s' -> '%s'", source, dest)
local success, err = action_fn(source, dest)
if not success then
notify.error("Could not " .. action .. " " .. notify.render_path(source) .. " - " .. (err or "???"))
end
find_file(utils.path_remove_trailing(dest))
end

---@param node Node
Expand Down Expand Up @@ -196,23 +148,119 @@ function Clipboard:clear_clipboard()
self.explorer.renderer:draw()
end

---Copy one node
---@param node Node
function Clipboard:copy(node)
utils.array_remove(self.data.cut, node)
toggle(node, self.data.copy)
---Bulk add/remove nodes to/from a clipboard list.
---@private
---@param nodes Node[] filtered nodes to operate on
---@param from Node[] list to remove from (the opposite clipboard)
---@param to Node[] list to add to
---@param verb string notification verb ("added to" or "cut to")
function Clipboard:bulk_clipboard(nodes, from, to, verb)
local added = 0
local removed = 0
for _, node in ipairs(nodes) do
if node.name ~= ".." then
utils.array_remove(from, node)
if utils.array_remove(to, node) then
removed = removed + 1
else
table.insert(to, node)
added = added + 1
end
end
end
if added > 0 then
notify.info(string.format("%d nodes %s clipboard.", added, verb))
elseif removed > 0 then
notify.info(string.format("%d nodes removed from clipboard.", removed))
end
self.explorer.renderer:draw()
end

---Cut one node
---@param node Node
function Clipboard:cut(node)
utils.array_remove(self.data.copy, node)
toggle(node, self.data.cut)
self.explorer.renderer:draw()
---Copy one or more nodes
---@param node_or_nodes Node|Node[]
function Clipboard:copy(node_or_nodes)
if type(node_or_nodes) == "table" and node_or_nodes.is and node_or_nodes:is(Node) then
utils.array_remove(self.data.cut, node_or_nodes)
toggle(node_or_nodes, self.data.copy)
self.explorer.renderer:draw()
else
self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.cut, self.data.copy, "added to")
end
end

---Paste cut or cop
---Cut one or more nodes
---@param node_or_nodes Node|Node[]
function Clipboard:cut(node_or_nodes)
if type(node_or_nodes) == "table" and node_or_nodes.is and node_or_nodes:is(Node) then
utils.array_remove(self.data.copy, node_or_nodes)
toggle(node_or_nodes, self.data.cut)
self.explorer.renderer:draw()
else
self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.copy, self.data.cut, "cut to")
end
end

---Clear clipboard for action and reload to reflect filesystem changes from paste.
---@private
---@param action ClipboardAction
function Clipboard:finish_paste(action)
self.data[action] = {}
self.explorer:reload_explorer()
end

---Resolve conflicting paste items with a single batch prompt.
---@private
---@param conflict {node: Node, dest: string}[]
---@param destination string
---@param action ClipboardAction
---@param action_fn ClipboardActionFn
function Clipboard:resolve_conflicts(conflict, destination, action, action_fn)
local prompt_select = #conflict .. " file(s) already exist"
local prompt_input = prompt_select .. ". R(ename suffix)/y/n: "

lib.prompt(prompt_input, prompt_select,
{ "", "y", "n" },
{ "Rename (suffix)", "Overwrite all", "Skip all" },
"nvimtree_paste_conflict",
function(item_short)
utils.clear_prompt()
if item_short == "y" then
for _, item in ipairs(conflict) do
do_paste_one(item.node.absolute_path, item.dest, action, action_fn)
end
self:finish_paste(action)
elseif item_short == "" or item_short == "r" then
vim.ui.input({ prompt = "Suffix: " }, function(suffix)
utils.clear_prompt()
if not suffix or suffix == "" then
return
end
local still_conflict = {}
for _, item in ipairs(conflict) do
local basename = vim.fn.fnamemodify(item.node.name, ":r")
local extension = vim.fn.fnamemodify(item.node.name, ":e")
local new_name = extension ~= "" and (basename .. suffix .. "." .. extension) or (item.node.name .. suffix)
local new_dest = utils.path_join({ destination, new_name })
local stats = vim.loop.fs_stat(new_dest)
if stats then
table.insert(still_conflict, { node = item.node, dest = new_dest })
else
do_paste_one(item.node.absolute_path, new_dest, action, action_fn)
end
end
if #still_conflict > 0 then
self:resolve_conflicts(still_conflict, destination, action, action_fn)
else
self:finish_paste(action)
end
end)
else
self:finish_paste(action)
end
end)
end

---Paste cut or copy with batch conflict resolution.
---@private
---@param node Node
---@param action ClipboardAction
Expand Down Expand Up @@ -243,14 +291,29 @@ function Clipboard:do_paste(node, action, action_fn)
destination = vim.fn.fnamemodify(destination, ":p:h")
end

-- Partition into conflict / no-conflict
local no_conflict = {}
local conflict = {}
for _, _node in ipairs(clip) do
local dest = utils.path_join({ destination, _node.name })
do_single_paste(_node.absolute_path, dest, action, action_fn)
local dest_stats = vim.loop.fs_stat(dest)
if dest_stats then
table.insert(conflict, { node = _node, dest = dest })
else
table.insert(no_conflict, { node = _node, dest = dest })
end
end

self.data[action] = {}
if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer()
-- Paste non-conflicting items immediately
for _, item in ipairs(no_conflict) do
do_paste_one(item.node.absolute_path, item.dest, action, action_fn)
end

-- Resolve conflicts in batch
if #conflict > 0 then
self:resolve_conflicts(conflict, destination, action, action_fn)
else
self:finish_paste(action)
end
end

Expand Down
64 changes: 51 additions & 13 deletions lua/nvim-tree/actions/fs/remove-file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ local notify = require("nvim-tree.notify")

local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local Node = require("nvim-tree.node")
local RootNode = require("nvim-tree.node.root")

local M = {
config = {},
Expand Down Expand Up @@ -126,9 +128,10 @@ function M.remove(node)
notify.info(notify_node .. " was properly removed.")
end

---Remove a single node with confirmation.
---@param node Node
function M.fn(node)
if node.name == ".." then
local function remove_one(node)
if node:is(RootNode) then
return
end

Expand All @@ -142,17 +145,7 @@ function M.fn(node)

if M.config.ui.confirm.remove then
local prompt_select = "Remove " .. node.name .. "?"
local prompt_input, items_short, items_long

if M.config.ui.confirm.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
local prompt_input, items_short, items_long = utils.confirm_prompt(prompt_select, M.config.ui.confirm.default_yes)

lib.prompt(prompt_input, prompt_select, items_short, items_long, "nvimtree_remove", function(item_short)
utils.clear_prompt()
Expand All @@ -165,6 +158,51 @@ function M.fn(node)
end
end

---Remove multiple nodes with a single confirmation prompt.
---@param nodes Node[]
local function remove_many(nodes)
if #nodes == 0 then
return
end

nodes = utils.filter_descendant_nodes(nodes)

local function execute()
for _, node in ipairs(nodes) do
if not node:is(RootNode) then
M.remove(node)
end
end
local explorer = core.get_explorer()
if not M.config.filesystem_watchers.enable and explorer then
explorer:reload_explorer()
end
end

if M.config.ui.confirm.remove then
local prompt_select = string.format("Remove %d selected?", #nodes)
local prompt_input, items_short, items_long = utils.confirm_prompt(prompt_select, M.config.ui.confirm.default_yes)

lib.prompt(prompt_input, prompt_select, items_short, items_long, "nvimtree_remove", function(item_short)
utils.clear_prompt()
if item_short == "y" or item_short == (M.config.ui.confirm.default_yes and "") then
execute()
end
end)
else
execute()
end
end

---@param node_or_nodes Node|Node[]
function M.fn(node_or_nodes)
if type(node_or_nodes) == "table" and node_or_nodes.is and node_or_nodes:is(Node) then
remove_one(node_or_nodes)
else
remove_many(node_or_nodes)
end
end

function M.setup(opts)
M.config.ui = opts.ui
M.config.actions = opts.actions
Expand Down
Loading