Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
37 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
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
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
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
26 changes: 22 additions & 4 deletions doc/nvim-tree-lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ 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()|
`ge` n Copy Basename |nvim_tree.api.fs.copy.basename()|
`gy` nx Copy Absolute Path |nvim_tree.api.fs.copy.absolute_path()|
`ge` nx 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()|
`J` n Last Sibling |nvim_tree.api.node.navigate.sibling.last()|
Expand All @@ -154,6 +154,8 @@ 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 Paste Using Register |nvim_tree.api.fs.paste_from_register_copying()|
`gx` n Move Using Register |nvim_tree.api.fs.paste_from_register_cutting()|
`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,8 +437,8 @@ 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", "ge", api.fs.copy.basename, opts("Copy Basename"))
vim.keymap.set({ "n", "x" }, "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set({ "n", "x" }, "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"))
vim.keymap.set("n", "J", api.node.navigate.sibling.last, opts("Last Sibling"))
Expand All @@ -447,6 +449,8 @@ 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.paste_from_register_copying, opts("Paste Using Register"))
vim.keymap.set("n", "gx", api.fs.paste_from_register_cutting, opts("Move Using Register"))
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 @@ -2467,6 +2471,20 @@ paste({node}) *nvim_tree.api.fs.paste()*
Parameters: ~
• {node} (`nvim_tree.api.Node?`)

*nvim_tree.api.fs.paste_from_register_copying()*
paste_from_register_copying({node_or_nodes})
Paste nodes from the system register as files to nvim-tree while copying.

Parameters: ~
• {node_or_nodes} (`nvim_tree.api.Node|nvim_tree.api.Node[]?`)

*nvim_tree.api.fs.paste_from_register_cutting()*
paste_from_register_cutting({node_or_nodes})
Paste nodes from the system register as files to nvim-tree while cutting.

Parameters: ~
• {node_or_nodes} (`nvim_tree.api.Node|nvim_tree.api.Node[]?`)

print_clipboard() *nvim_tree.api.fs.print_clipboard()*
Print the contents of the nvim-tree clipboard.

Expand Down
12 changes: 12 additions & 0 deletions lua/nvim-tree/_meta/api/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ function nvim_tree.api.fs.cut(node) end
---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.paste(node) end

---
---Paste nodes from the system register as files to nvim-tree while copying.
---
---@param node_or_nodes? nvim_tree.api.Node | nvim_tree.api.Node[]
function nvim_tree.api.fs.paste_from_register_copying(node_or_nodes) end

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.

Do we still need these two? They don't appear to be used. p does the job now :)


---
---Paste nodes from the system register as files to nvim-tree while cutting.
---
---@param node_or_nodes? nvim_tree.api.Node | nvim_tree.api.Node[]
function nvim_tree.api.fs.paste_from_register_cutting(node_or_nodes) end

---
---Print the contents of the nvim-tree clipboard.
---
Expand Down
2 changes: 0 additions & 2 deletions lua/nvim-tree/_meta/config/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ error("Cannot require a meta file")
---[nvim_tree.config.actions.remove_file]
---@field remove_file? nvim_tree.config.actions.remove_file



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.

I'm guessing the style tooling fixed this.

--- vim [current-directory] behaviour
---@class nvim_tree.config.actions.change_dir
---
Expand Down
113 changes: 92 additions & 21 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 @@ -43,6 +44,10 @@ function Clipboard:new(args)
self.reg = self.explorer.opts.actions.use_system_clipboard and "+" or "1"
end

---@class PasteOptions
---@field use_register? boolean
---@field cut? boolean

---@param source string
---@param destination string
---@return boolean
Expand Down Expand Up @@ -176,10 +181,20 @@ 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)

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.

I was pretty hard on you about the node_attribute_getters refactor.

This refactor is good as it is necessary to avoid (already) duplicated code for the new feature.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was exactly the purpose of the refactor, that's why I did it. maybe we can do it on the future.

if type(node_or_nodes) == "table" and node_or_nodes.is and node_or_nodes:is(Node) then
return false
end
return true
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()
Expand All @@ -191,7 +206,7 @@ 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()
Expand Down Expand Up @@ -304,12 +319,37 @@ 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), ",")

for _, absolute_path in ipairs(absolute_paths) do
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))

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.

Creating (dummy) nodes is a smart way to work with the existing clipboard code to avoid massive changes.

We do need to destroy Node:destroy() these nodes when we are done, to release resources e.g. watchers.

They are mixed in with current instance nodes in the clip and conflict tables in Clipboard:do_paste so we cannot just destroy all of those nodes.

Suggestion: keep the dummy nodes from self:get_nodes_from_reg() in another table. Destroy each node in the table at:

  • early exit around if not stats and err_name ~= "ENOENT" then
  • the end of Clipboard:do_paste

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I called destroy to everything on the clip variable itself and everything looks fine.

else
table.insert(nodes, FileNode(node_args))
end
end
return nodes
end

---Paste cut or copy with batch conflict resolution.
---@private
---@param node Node
---@param action ClipboardAction
---@param action_fn ClipboardActionFn
function Clipboard:do_paste(node, action, action_fn)
---@param opts? PasteOptions
function Clipboard:do_paste(node, action, action_fn, opts)
if node.name == ".." then
node = self.explorer
else
Expand All @@ -318,7 +358,7 @@ function Clipboard:do_paste(node, action, action_fn)
node = dir:last_group_node()
end
end
local clip = self.data[action]
local clip = opts and opts.use_register and self:get_nodes_from_reg() or self.data[action]
if #clip == 0 then
return
end
Expand Down Expand Up @@ -386,11 +426,12 @@ end

---Paste cut (if present) or copy (if present)
---@param node Node
function Clipboard:paste(node)
if self.data.cut[1] ~= nil then
self:do_paste(node, "cut", do_cut)
elseif self.data.copy[1] ~= nil then
self:do_paste(node, "copy", do_copy)
---@param opts? PasteOptions
function Clipboard:paste(node, opts)
if self.data.cut[1] ~= nil or opts and opts.use_register and opts.cut then
self:do_paste(node, "cut", do_cut, opts)
elseif self.data.copy[1] ~= nil or opts and opts.use_register then
self:do_paste(node, "copy", do_copy, opts)
end
end

Expand All @@ -413,7 +454,8 @@ function Clipboard:print_clipboard()
end

---@param content string
function Clipboard:copy_to_reg(content)
---@param msg? string
function Clipboard:copy_to_reg(content, msg)
-- 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)
Expand All @@ -423,7 +465,7 @@ function Clipboard:copy_to_reg(content)
end)
vim.api.nvim_buf_delete(temp_buf, {})

notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
notify.info(msg or string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
end

---@param node Node
Expand All @@ -437,15 +479,22 @@ function Clipboard:copy_filename(node)
end
end

---@param node Node
function Clipboard:copy_basename(node)
if node.name == ".." then
-- root
self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r"))
---@param node_or_nodes Node|Node[]
function Clipboard:copy_basename(node_or_nodes)
local content = ""
if self:is_nodes_array(node_or_nodes) == false or #node_or_nodes == 1 then
local node = #node_or_nodes == 1 and node_or_nodes[0] or node_or_nodes
content = node:get_basename()
else
-- node
self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
for i, node in ipairs(node_or_nodes) do
if i == 1 then
content = node:get_basename()
else
content = content .. "," .. node:get_basename()
end
end
end
self:copy_to_reg(content)
end

---@param node Node
Expand All @@ -470,15 +519,37 @@ function Clipboard:copy_path(node)
end
end

---@private
---@param node Node
function Clipboard:copy_absolute_path(node)
---@return string
function Clipboard:get_absolute_path(node)
if node.name == ".." then
node = self.explorer
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)
return node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path
end

---@param node_or_nodes Node|Node[]
function Clipboard:copy_absolute_path(node_or_nodes)
local content = ""
local is_single = self:is_nodes_array(node_or_nodes) == false or #node_or_nodes == 1
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)
for i, node in ipairs(node_or_nodes) do
if i == 1 then
content = self:get_absolute_path(node)
else
content = content .. "," .. self:get_absolute_path(node)
end
end
end

self:copy_to_reg(content, string.format("%s paths copied to register", is_single and 1 or #node_or_nodes))
end

---Node is cut. Will not be copied.
Expand Down
6 changes: 4 additions & 2 deletions lua/nvim-tree/api/impl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,16 @@ 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.basename = en(function(e, n) e.clipboard:copy_basename(n) end)
api.fs.copy.absolute_path = ev(function(e, n) e.clipboard:copy_absolute_path(n) end)
api.fs.copy.basename = ev(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.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.paste_from_register_copying = en(function(e, n) e.clipboard:paste(n, { use_register = true }) end)
api.fs.paste_from_register_cutting = en(function(e, n) e.clipboard:paste(n, { use_register = true, 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
6 changes: 4 additions & 2 deletions lua/nvim-tree/keymap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ 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", "ge", api.fs.copy.basename, opts("Copy Basename"))
vim.keymap.set({ "n", "x" }, "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set({ "n", "x" }, "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"))
vim.keymap.set("n", "J", api.node.navigate.sibling.last, opts("Last Sibling"))
Expand All @@ -102,6 +102,8 @@ 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.paste_from_register_copying, opts("Paste Using Register"))
vim.keymap.set("n", "gx", api.fs.paste_from_register_cutting, opts("Move Using Register"))
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
10 changes: 10 additions & 0 deletions lua/nvim-tree/node/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,14 @@ function Node:expand(expand_opts)
end
end

function Node:get_basename()
if self.name == ".." then

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.

On further reflection, changing the ".." makes me nervous. Yes it's legacy, however it is present in many places in Clipboard hence we shouldn't change it when building this feature.

Let's keep it localised - please move this method to Clipboard so that it doesn't spread to other functionality.

-- root
return vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r")
else
-- node
return vim.fn.fnamemodify(self.name, ":r")
end
end

return Node
Loading