Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ddbc033
feat: add copy absolute path with protocol into public api
Uanela Jun 11, 2026
c842a08
feat: add helper get_basename into node
Uanela Jun 11, 2026
058e508
feat: add gby keymap, and allow visual mode for copy.basename|absolut…
Uanela Jun 11, 2026
89edce6
feat(clipboard): add opts for adding protocol in reg operations
Uanela Jun 11, 2026
b98e0c4
feat(api): allow visual operation in absolute_path*
Uanela Jun 11, 2026
f2269a1
feat: add paste with protocol to keymap+api
Uanela Jun 11, 2026
cdc7b28
feat: add paste with protocol to clipboard
Uanela Jun 11, 2026
38ada46
feat: add configurable clipboard protocol
Uanela Jun 11, 2026
b9e9b7a
fix: filter descendant nodes on copy_absolute_path_with_protocol
Uanela Jun 11, 2026
877e726
refactor: change clip protocol keymaps to bgy and bgp
Uanela Jun 11, 2026
885b4d9
wip
Uanela Jun 11, 2026
2c56cc6
fix(clipboad): correctly show the number of copied nodes on bgy
Uanela Jun 11, 2026
77325e5
chore: remove bgy, bgp and add gp + gx
Uanela Jun 14, 2026
a9253de
chore: update api/fs.lua and api impl.lua to match new keymaps
Uanela Jun 14, 2026
0fd8cfa
fix: correctly pass the function to new keymaps
Uanela Jun 14, 2026
073fac1
chore: remove use_protocol and use use_register + cut options
Uanela Jun 14, 2026
27f5254
chore: update docs
Uanela Jun 14, 2026
b5750c8
feat: add download feat when `gp`
Uanela Jun 15, 2026
43f01b5
Merge branch 'master' into chore/remove-operation-between-instances-p…
Uanela Jun 15, 2026
4f787e0
chore: move node:get_basename to cliboard_
Uanela Jun 18, 2026
a7e647b
chore(clipboard): remove vim.cmd workaround in favor of vim.fn.setreg
Uanela Jun 18, 2026
ba8e7a1
chore: use newline as separator instead of comma
Uanela Jun 18, 2026
f79cdcb
refactor(clipboard): simplify copying node attribute and allow all to…
Uanela Jun 18, 2026
356c083
refactor(keymap): remove gx, use gp to move
Uanela Jun 18, 2026
6f1b818
feat(clipboard): always copy/cut to register and make `p` pick local …
Uanela Jun 18, 2026
887e6c1
refactor(docs): update api docs and rename paste_while_cutting to move
Uanela Jun 18, 2026
859f733
fix: correctly format keymaps
Uanela Jun 18, 2026
b1662a5
chore: regenerate docs
Uanela Jun 18, 2026
8d1adf0
Merge branch 'chore/remove-operation-between-instances-protocol' of h…
Uanela Jun 18, 2026
f8fa340
chore: add ... when showing Downloading message
Uanela Jun 18, 2026
9c782a1
Update lua/nvim-tree/actions/fs/clipboard.lua
Uanela Jun 18, 2026
0522def
Correct is_nodes_array return logic
Uanela Jun 18, 2026
5eba88e
Merge branch 'master' into chore/remove-operation-between-instances-p…
Uanela Jun 22, 2026
fa2b186
fix: remove unused apis
Uanela Jun 22, 2026
4bd13ec
chore: copy clipboard.data to register on copy/cut
Uanela Jun 22, 2026
062a2d2
chore: revert copy_node_attribute
Uanela Jun 22, 2026
f10f779
refactor: remove visual mode from copy.basename|filename|path
Uanela Jun 22, 2026
30c5036
Merge branch 'chore/remove-operation-between-instances-protocol' of h…
Uanela Jun 22, 2026
1b67371
chore: update docs
Uanela Jun 22, 2026
4fcbafe
chore: add fs.move api and update docs
Uanela Jun 25, 2026
4b13775
chore: destroy dummy nodes and prevent empty paths nodes parsing
Uanela Jun 25, 2026
cb58a07
fix: _meta/api/fs.lua and docs
Uanela Jun 25, 2026
582bc35
Merge branch 'chore/remove-operation-between-instances-protocol' of h…
Uanela Jun 29, 2026
91a288c
Merge branch 'feat/add-download-from-path' of https://github.com/nvim…
Uanela Jun 29, 2026
9cad29b
fix(clipboard): destroy dummy nodes from register on paste
Uanela Jun 30, 2026
ee173eb
Merge branch 'chore/remove-operation-between-instances-protocol' of h…
Uanela Jun 30, 2026
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
23 changes: 19 additions & 4 deletions doc/nvim-tree-lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Features
- Git integration
- Diagnostics integration: LSP and COC
- (Live) filtering
- Cut, copy, paste, rename, delete, create
- Rename, delete, create
- Cut, copy, paste locally and between instances
- Highly customisable

File Icons
Expand Down Expand Up @@ -142,7 +143,7 @@ Show the mappings: `g?`
`F` n Live Filter: Clear |nvim_tree.api.filter.live.clear()|
`f` n Live Filter: Start |nvim_tree.api.filter.live.start()|
`g?` n Help |nvim_tree.api.tree.toggle_help()|
`gy` n Copy Absolute Path |nvim_tree.api.fs.copy.absolute_path()|
`gy` nx Copy Absolute Path |nvim_tree.api.fs.copy.absolute_path()|
`ge` n Copy Basename |nvim_tree.api.fs.copy.basename()|
`H` n Toggle Filter: Dotfiles |nvim_tree.api.filter.dotfiles.toggle()|
`I` n Toggle Filter: Git Ignored |nvim_tree.api.filter.git.ignored.toggle()|
Expand All @@ -154,6 +155,7 @@ Show the mappings: `g?`
`o` n Open |nvim_tree.api.node.open.edit()|
`O` n Open: No Window Picker |nvim_tree.api.node.open.no_window_picker()|
`p` n Paste |nvim_tree.api.fs.paste()|
`gp` n Move |nvim_tree.api.fs.move()|
`P` n Parent Directory |nvim_tree.api.node.navigate.parent()|
`q` n Close |nvim_tree.api.tree.close()|
`r` n Rename |nvim_tree.api.fs.rename()|
Expand Down Expand Up @@ -435,7 +437,7 @@ You are encouraged to copy these to your {on_attach} function. >lua
vim.keymap.set("n", "F", api.filter.live.clear, opts("Live Filter: Clear"))
vim.keymap.set("n", "f", api.filter.live.start, opts("Live Filter: Start"))
vim.keymap.set("n", "g?", api.tree.toggle_help, opts("Help"))
vim.keymap.set("n", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set({ "n", "x" }, "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set("n", "ge", api.fs.copy.basename, opts("Copy Basename"))
vim.keymap.set("n", "H", api.filter.dotfiles.toggle, opts("Toggle Filter: Dotfiles"))
vim.keymap.set("n", "I", api.filter.git.ignored.toggle, opts("Toggle Filter: Git Ignored"))
Expand All @@ -447,6 +449,7 @@ You are encouraged to copy these to your {on_attach} function. >lua
vim.keymap.set("n", "o", api.node.open.edit, opts("Open"))
vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker"))
vim.keymap.set("n", "p", api.fs.paste, opts("Paste"))
vim.keymap.set("n", "gp", api.fs.move, opts("Move"))
vim.keymap.set("n", "P", api.node.navigate.parent, opts("Parent Directory"))
vim.keymap.set("n", "q", api.tree.close, opts("Close"))
vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))
Expand Down Expand Up @@ -2459,8 +2462,20 @@ cut({node}) *nvim_tree.api.fs.cut()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`)

move({node}) *nvim_tree.api.fs.move()*
Paste while cutting files from:
• current nvim-tree clipboard (if present)
• another nvim-tree instance via system clipboard

If {node} is a file it will pasted in the parent directory.

Parameters: ~
• {node} (`nvim_tree.api.Node?`)

paste({node}) *nvim_tree.api.fs.paste()*
Paste from the nvim-tree clipboard.
Paste files from:
• current nvim-tree clipboard (if present)
• another nvim-tree instance via system clipboard

If {node} is a file it will pasted in the parent directory.

Expand Down
14 changes: 13 additions & 1 deletion lua/nvim-tree/_meta/api/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,25 @@ function nvim_tree.api.fs.create(node) end
function nvim_tree.api.fs.cut(node) end

---
---Paste from the nvim-tree clipboard.
---Paste files from:
---- current nvim-tree clipboard (if present)
---- another nvim-tree instance via system clipboard
---
---If {node} is a file it will pasted in the parent directory.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.paste(node) end

---
---Paste while cutting files from:
---- current nvim-tree clipboard (if present)
---- another nvim-tree instance via system clipboard
---
---If {node} is a file it will pasted in the parent directory.
---
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.move(node) end

---
---Print the contents of the nvim-tree clipboard.
---
Expand Down
155 changes: 125 additions & 30 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 FileNode = require("nvim-tree.node.file")
local Node = require("nvim-tree.node")

---@alias ClipboardAction "copy" | "cut"
Expand Down Expand Up @@ -176,28 +177,37 @@ function Clipboard:bulk_clipboard(nodes, from, to, verb)
self.explorer.renderer:draw()
end

---@private
---@param node_or_nodes Node|Node[]
---@return boolean
function Clipboard:is_nodes_array(node_or_nodes)
return type(node_or_nodes) == "table" and node_or_nodes.is and node_or_nodes:is(Node)
end

---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
if self:is_nodes_array(node_or_nodes) == false 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
self:copy_absolute_path(self.data.copy, { notify = false })
end

---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
if self:is_nodes_array(node_or_nodes) == false 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
self:copy_absolute_path(self.data.cut, { notify = false })
end

---Clear clipboard for action and reload to reflect filesystem changes from paste.
Expand Down Expand Up @@ -304,6 +314,40 @@ function Clipboard:resolve_conflicts(conflict, destination, action, action_fn)
end)
end

--- Transforms the copied absolute paths on register to node
---@private
function Clipboard:get_nodes_from_reg()
local content = vim.fn.getreg(self.reg)

if #content == 0 then
return {}
end

local nodes = {}
local absolute_paths = vim.split(content:sub(1, #content), "\n")

for _, absolute_path in ipairs(absolute_paths) do
if absolute_path:match("^%s*(.-)%s*s") then
local node_args = { absolute_path = absolute_path, name = vim.fn.fnamemodify(absolute_path, ":t"), explorer = self.explorer }
if absolute_path:sub(-1) == "/" then
node_args.name = vim.fn.fnamemodify(absolute_path:sub(1, -2), ":t")
table.insert(nodes, DirectoryNode(node_args))
else
table.insert(nodes, FileNode(node_args))
end
end
end
return nodes
end

---@param nodes Node[]
---@private
function Clipboard:destroy_nodes(nodes)
for _, n in ipairs(nodes) do
n:destroy()
end
end

---Paste cut or copy with batch conflict resolution.
---@private
---@param node Node
Expand All @@ -318,7 +362,9 @@ function Clipboard:do_paste(node, action, action_fn)
node = dir:last_group_node()
end
end
local clip = self.data[action]
local is_local = #self.data[action] > 0
local clip = is_local and self.data[action] or self:get_nodes_from_reg()

if #clip == 0 then
return
end
Expand All @@ -328,6 +374,9 @@ function Clipboard:do_paste(node, action, action_fn)
if not stats and err_name ~= "ENOENT" then
log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err)
notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???"))
if ~is_local then
self:destroy_nodes(clip)
end
return
end
local is_dir = stats and stats.type == "directory"
Expand All @@ -350,7 +399,18 @@ function Clipboard:do_paste(node, action, action_fn)

-- Paste non-conflicting items immediately
for _, item in ipairs(no_conflict) do
do_paste_one(item.node.absolute_path, item.dest, action, action_fn)
local absolute_path = item.node.absolute_path
if absolute_path:sub(1, #"http") == "http" then
notify.info("Downloading " .. absolute_path .. " to " .. item.dest .. "...")
local result = vim.fn.system({ "curl", "-sL", absolute_path, "-o", item.dest })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as it pains me to talk about netrw, it appears that it handle almost all protocols:

e.g.

nvim --clean
:Nread https://raw.githubusercontent.com/nvim-tree/nvim-tree.lua/refs/heads/master/README.md

Perhaps we could invoke it to do all the work.

if vim.v.shell_error == 0 then
notify.info("Downloaded " .. absolute_path .. " successfully at " .. item.dest)
else
notify.error("Error downloading " .. absolute_path .. ": " .. result)
end
else
do_paste_one(absolute_path, item.dest, action, action_fn)
end
end

-- Resolve conflicts in batch
Expand All @@ -359,6 +419,10 @@ function Clipboard:do_paste(node, action, action_fn)
else
self:finish_paste(action)
end

if ~is_local then
self:destroy_nodes(clip)
end
end

---@param source string
Expand Down Expand Up @@ -386,10 +450,12 @@ end

---Paste cut (if present) or copy (if present)
---@param node Node
function Clipboard:paste(node)
if self.data.cut[1] ~= nil then
---@param opts? { cut?: boolean }
function Clipboard:paste(node, opts)
opts = opts and opts or {}
if self.data.cut[1] ~= nil or opts.cut == true then
self:do_paste(node, "cut", do_cut)
elseif self.data.copy[1] ~= nil then
else
self:do_paste(node, "copy", do_copy)
end
end
Expand All @@ -413,17 +479,15 @@ function Clipboard:print_clipboard()
end

---@param content string
function Clipboard:copy_to_reg(content)
-- manually firing TextYankPost does not set vim.v.event
-- workaround: create a scratch buffer with the clipboard contents and send a yank command
local temp_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content })
vim.api.nvim_buf_call(temp_buf, function()
vim.cmd(string.format('normal! "%sy$', self.reg))
end)
vim.api.nvim_buf_delete(temp_buf, {})

notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
---@param message? string
---@param opts? { notify?: boolean }
function Clipboard:copy_to_reg(content, message, opts)
opts = opts and opts or {}
vim.fn.setreg(self.reg, type(content) == "table" and content or { content }, "v")

if opts.notify ~= false then
notify.info(message or string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
end
end

---@param node Node
Expand All @@ -437,18 +501,20 @@ function Clipboard:copy_filename(node)
end
end

---@private
---@param node Node
function Clipboard:copy_basename(node)
---@return string
function Clipboard:get_absolute_path(node)
if node.name == ".." then
-- root
self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r"))
else
-- node
self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
node = self.explorer
end

local absolute_path = node.absolute_path
return node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path
end

---@param node Node
---@return string|nil
function Clipboard:copy_path(node)
if node.name == ".." then
-- root
Expand All @@ -470,15 +536,44 @@ function Clipboard:copy_path(node)
end
end

---@param node_or_nodes Node|Node[]
---@param opts? { notify?: boolean }
function Clipboard:copy_absolute_path(node_or_nodes, opts)
opts = opts and opts or {}
local content

local is_single = self:is_nodes_array(node_or_nodes) == false
if is_single then
local node = #node_or_nodes == 1 and node_or_nodes[0] or node_or_nodes
content = self:get_absolute_path(node)
else
node_or_nodes = utils.filter_descendant_nodes(node_or_nodes)
content = {}
for _, node in ipairs(node_or_nodes) do
table.insert(content, self:get_absolute_path(node))
end
end


if content ~= nil then
local message = nil

if not is_single then
message = string.format("%s %s copied to register", #content, #content > 1 and "absolute paths" or "absolute path")
end
self:copy_to_reg(content, message, opts)
end
end

---@param node Node
function Clipboard:copy_absolute_path(node)
function Clipboard:copy_basename(node)
if node.name == ".." then
node = self.explorer
-- root
self:copy_to_reg(vim.fn.fnamemodify(node.explorer.absolute_path, ":t:r"))
else
-- node
self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
end

local absolute_path = node.absolute_path
local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path
self:copy_to_reg(content)
end

---Node is cut. Will not be copied.
Expand Down
5 changes: 3 additions & 2 deletions lua/nvim-tree/api/impl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,15 @@ function M.hydrate_post_setup(api)
api.filter.toggle = e_(function(e) e.filters:toggle() end)

api.fs.clear_clipboard = e_(function(e) e.clipboard:clear_clipboard() end)
api.fs.copy.absolute_path = en(function(e, n) e.clipboard:copy_absolute_path(n) end)
api.fs.copy.absolute_path = ev(function(e, n) e.clipboard:copy_absolute_path(n) end)
api.fs.copy.basename = en(function(e, n) e.clipboard:copy_basename(n) end)
api.fs.copy.filename = en(function(e, n) e.clipboard:copy_filename(n) end)
api.fs.copy.node = ev(function(e, n) e.clipboard:copy(n) end)
api.fs.copy.relative_path = en(function(e, n) e.clipboard:copy_path(n) end)
api.fs.copy.node = ev(function(e, n) e.clipboard:copy(n) end)
api.fs.create = _n(function(n) require("nvim-tree.actions.fs.create-file").fn(n) end)
api.fs.cut = ev(function(e, n) e.clipboard:cut(n) end)
api.fs.paste = en(function(e, n) e.clipboard:paste(n) end)
api.fs.move = en(function(e, n) e.clipboard:paste(n, { cut = true }) end)
api.fs.print_clipboard = e_(function(e) e.clipboard:print_clipboard() end)
api.fs.remove = _v(function(n) require("nvim-tree.actions.fs.remove-file").fn(n) end)
api.fs.rename = _n(function(n) require("nvim-tree.actions.fs.rename-file").rename_node(n) end)
Expand Down
3 changes: 2 additions & 1 deletion lua/nvim-tree/keymap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function M.on_attach_default(bufnr)
vim.keymap.set("n", "F", api.filter.live.clear, opts("Live Filter: Clear"))
vim.keymap.set("n", "f", api.filter.live.start, opts("Live Filter: Start"))
vim.keymap.set("n", "g?", api.tree.toggle_help, opts("Help"))
vim.keymap.set("n", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set({ "n", "x" }, "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set("n", "ge", api.fs.copy.basename, opts("Copy Basename"))
vim.keymap.set("n", "H", api.filter.dotfiles.toggle, opts("Toggle Filter: Dotfiles"))
vim.keymap.set("n", "I", api.filter.git.ignored.toggle, opts("Toggle Filter: Git Ignored"))
Expand All @@ -102,6 +102,7 @@ function M.on_attach_default(bufnr)
vim.keymap.set("n", "o", api.node.open.edit, opts("Open"))
vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker"))
vim.keymap.set("n", "p", api.fs.paste, opts("Paste"))
vim.keymap.set("n", "gp", api.fs.move, opts("Move"))
vim.keymap.set("n", "P", api.node.navigate.parent, opts("Parent Directory"))
vim.keymap.set("n", "q", api.tree.close, opts("Close"))
vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))
Expand Down
Loading