Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
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
32 changes: 24 additions & 8 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,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 All @@ -164,8 +165,8 @@ Show the mappings: `g?`
`U` n Toggle Filter: Custom |nvim_tree.api.filter.custom.toggle()|
`W` n Collapse All |nvim_tree.api.tree.collapse_all()|
`x` nx Cut |nvim_tree.api.fs.cut()|
`y` n Copy Name |nvim_tree.api.fs.copy.filename()|
`Y` n Copy Relative Path |nvim_tree.api.fs.copy.relative_path()|
`y` nx Copy Name |nvim_tree.api.fs.copy.filename()|
`Y` nx Copy Relative Path |nvim_tree.api.fs.copy.relative_path()|
`<2-LeftMouse>` n Open |nvim_tree.api.node.open.edit()|
`<2-RightMouse>` n CD |nvim_tree.api.tree.change_root_to_node()|

Expand Down Expand Up @@ -435,8 +436,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 +448,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 All @@ -457,8 +459,8 @@ You are encouraged to copy these to your {on_attach} function. >lua
vim.keymap.set("n", "U", api.filter.custom.toggle, opts("Toggle Filter: Custom"))
vim.keymap.set("n", "W", api.tree.collapse_all, opts("Collapse All"))
vim.keymap.set({ "n", "x" }, "x", api.fs.cut, opts("Cut"))
vim.keymap.set("n", "y", api.fs.copy.filename, opts("Copy Name"))
vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path"))
vim.keymap.set({ "n", "x" }, "y", api.fs.copy.filename, opts("Copy Name"))
vim.keymap.set({ "n", "x" }, "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"))
-- END_ON_ATTACH_DEFAULT
Expand Down Expand Up @@ -2467,6 +2469,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
148 changes: 114 additions & 34 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,40 @@ 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)
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()
else
self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.cut, self.data.copy, "added to")
end
self:copy_node_attribute(node_or_nodes, "absolute_path", { 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_node_attribute(node_or_nodes, "absolute_path", { notify = false })
end

---Clear clipboard for action and reload to reflect filesystem changes from paste.
Expand Down Expand Up @@ -304,6 +317,30 @@ 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
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
return nodes
end

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

if #clip == 0 then
return
end
Expand Down Expand Up @@ -386,10 +424,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,46 +453,73 @@ 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
function Clipboard:copy_filename(node)
---@return string
function Clipboard:get_node_filename(node)
if node.name == ".." then
-- root
self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t"))
return vim.fn.fnamemodify(self.explorer.absolute_path, ":t")
else
-- node
self:copy_to_reg(node.name)
return node.name
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[]
---@param attribute "absolute_path" | "basename" | "filename" | "relative_path"
---@param opts? { notify?: boolean }
function Clipboard:copy_node_attribute(node_or_nodes, attribute, opts)
opts = opts and opts or {}
local content
local node_attribute_getters = {
basename = function(n) return self:get_node_basename(n) end,
filename = function(n) return self:get_node_filename(n) end,
relative_path = function(n) return self:get_node_relative_path(n) end,
absolute_path = function(n) return self:get_node_absolute_path(n) end,
}
Comment on lines +485 to +490

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 really appreciate the care and effort you've made with this refactor, however:

  • it obfuscates the flow of control: it's no longer easy to "LSP References" get_node_relative_path to map it directly to API/impl
  • it's doesn't appear necessary to achieve the goal of copy between instances

Let's limit the scope of this change to the minimum required to achieve the functionality: atomic changes.

  • It ensures that we can look at git history and identify what was the new functionality and what was a refactor.
  • In the case of a problem it allows us to identify whether the problem was the functionality or the refactor.
  • Allows reverting/reapplying of individual changes in the event of an issue, instead of having to revert the larger change

I'm always open to refactors and polishing, however please raise separate PRs for them.


local attribute_getter = node_attribute_getters[attribute]

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 = attribute_getter(node)
else
-- node
self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
node_or_nodes = utils.filter_descendant_nodes(node_or_nodes)
content = {}
for _, node in ipairs(node_or_nodes) do
table.insert(content, attribute_getter(node))
end
end


if content ~= nil then
local message = nil

if not is_single then
message = string.format("%s %s copied to register", #content, attribute:gsub("_", " ") .. "s")
end
self:copy_to_reg(content, message, opts)
end
end

---@param node Node
function Clipboard:copy_path(node)
---@return string|nil
function Clipboard:get_node_relative_path(node)
if node.name == ".." then
-- root
self:copy_to_reg(utils.path_add_trailing(""))
return utils.path_add_trailing("")
else
-- node
local absolute_path = node.absolute_path
Expand All @@ -463,22 +530,35 @@ function Clipboard:copy_path(node)

local relative_path = utils.path_relative(absolute_path, cwd)
if node:is(DirectoryNode) then
self:copy_to_reg(utils.path_add_trailing(relative_path))
return utils.path_add_trailing(relative_path)
else
self:copy_to_reg(relative_path)
return relative_path
end
end
end

---@private
---@param node Node
function Clipboard:copy_absolute_path(node)
---@return string
function Clipboard:get_node_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 Node
---@return string
function Clipboard:get_node_basename(node)
if node.name == ".." then
-- root
return vim.fn.fnamemodify(node.explorer.absolute_path, ":t:r")
else
-- node
return vim.fn.fnamemodify(node.name, ":r")
end
end

---Node is cut. Will not be copied.
Expand Down
9 changes: 5 additions & 4 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.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.absolute_path = ev(function(e, n) e.clipboard:copy_node_attribute(n, "absolute_path") end)
api.fs.copy.basename = ev(function(e, n) e.clipboard:copy_node_attribute(n, "basename") end)
api.fs.copy.filename = ev(function(e, n) e.clipboard:copy_node_attribute(n, "filename") end)
api.fs.copy.relative_path = ev(function(e, n) e.clipboard:copy_node_attribute(n, "relative_path") 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.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
Loading
Loading