Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot]
531b807b8f chore(master): release nvim-tree 1.18.0 (#3318)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-01 12:53:44 +10:00
Uanela Como
c760c92667 feat(#1549): p will paste files from another nvim-tree instance, gp will cut/paste (#3336)
* feat: add copy absolute path with protocol into public api

* feat: add helper get_basename into node

* feat: add gby keymap, and allow visual mode for copy.basename|absolute_path

* feat(clipboard): add opts for adding protocol in reg operations

* feat(api): allow visual operation in absolute_path*

* feat: add paste with protocol to keymap+api

* feat: add paste with protocol to clipboard

* feat: add configurable clipboard protocol

* fix: filter descendant nodes on copy_absolute_path_with_protocol

* refactor: change clip protocol keymaps to bgy and bgp

* wip

* fix(clipboad): correctly show the number of copied nodes on bgy

* chore: remove bgy, bgp and add gp + gx

* chore: update api/fs.lua and api impl.lua to match new keymaps

* fix: correctly pass the function to new keymaps

* chore: remove use_protocol and use use_register + cut options

* chore: update docs

* chore: move node:get_basename to cliboard_

* chore(clipboard): remove vim.cmd workaround in favor of vim.fn.setreg

* chore: use newline as separator instead of comma

* refactor(clipboard): simplify copying node attribute and allow all to work in visual mode

* refactor(keymap): remove gx, use gp to move

* feat(clipboard): always copy/cut to register and make `p` pick local data or register data

* refactor(docs): update api docs and rename paste_while_cutting to move

* fix: correctly format keymaps

* chore: regenerate docs

* fix: remove unused apis

* chore: copy clipboard.data to register on copy/cut

* chore: revert copy_node_attribute

* refactor: remove visual mode from copy.basename|filename|path

* chore: update docs

* chore: add fs.move api and update docs

* chore: destroy dummy nodes and prevent empty paths nodes parsing

* fix: _meta/api/fs.lua and docs

* fix(clipboard): destroy dummy nodes from register on paste

* fix(clipboard): correctly check for local paste operation
2026-07-01 10:11:03 +10:00
dependabot[bot]
8e8be709c9 chore(deps): bump actions/checkout from 6 to 7 (#3340)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-20 13:16:28 +10:00
Alexander Courtis
fb343438d4 docs: generated help fields are sorted as of v0.12.3 (#3337)
* doc: generated help fields are sorted as of v0.12.3

* ci: add weekly trigger for CI job
2026-06-15 12:30:16 +10:00
10 changed files with 401 additions and 268 deletions

View File

@@ -5,6 +5,8 @@ on:
push: push:
branches: [master] branches: [master]
workflow_dispatch: workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # weekly on sundays
permissions: permissions:
contents: read contents: read
@@ -24,7 +26,7 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: install lua ${{ matrix.lua_version }} - name: install lua ${{ matrix.lua_version }}
uses: leafo/gh-actions-lua@v13 uses: leafo/gh-actions-lua@v13
@@ -58,7 +60,7 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: install nvim ${{ matrix.nvim_version }} - name: install nvim ${{ matrix.nvim_version }}
uses: rhysd/action-setup-vim@v1 uses: rhysd/action-setup-vim@v1

View File

@@ -10,7 +10,7 @@ jobs:
luarocks-upload: luarocks-upload:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: LuaRocks Upload - name: LuaRocks Upload
uses: nvim-neorocks/luarocks-tag-release@v7 uses: nvim-neorocks/luarocks-tag-release@v7
env: env:

View File

@@ -16,7 +16,7 @@ jobs:
steps: steps:
- uses: googleapis/release-please-action@v5 - uses: googleapis/release-please-action@v5
id: release id: release
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: tag major and minor versions - name: tag major and minor versions
if: ${{ steps.release.outputs.release_created }} if: ${{ steps.release.outputs.release_created }}
run: | run: |

View File

@@ -1,3 +1,3 @@
{ {
".": "1.17.0" ".": "1.18.0"
} }

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## [1.18.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.17.0...nvim-tree-v1.18.0) (2026-07-01)
### Features
* **#1549:** p will paste files from another nvim-tree instance, gp will cut/paste ([#3336](https://github.com/nvim-tree/nvim-tree.lua/issues/3336)) ([c760c92](https://github.com/nvim-tree/nvim-tree.lua/commit/c760c92667fe5febf99e2a5bb8a543b0208c743b))
### Bug Fixes
* **#3312:** correct cursor when full name shown, requires nvim 0.12.2+ ([#3316](https://github.com/nvim-tree/nvim-tree.lua/issues/3316)) ([d277467](https://github.com/nvim-tree/nvim-tree.lua/commit/d277467fc0d1d0e2bca88165a1de6b526f9f6fe8))
* **#3324:** deprecated BufModifiedSet event replaced by OptionSet event for Nvim 0.13+ ([#3325](https://github.com/nvim-tree/nvim-tree.lua/issues/3325)) ([f9bfc00](https://github.com/nvim-tree/nvim-tree.lua/commit/f9bfc0059eca24546b69a7006110463da4808f8f))
* **#3327:** pass nvim_tree.api.node.open.Opts to all applicable nvim_tree.api.node.open function ([#3329](https://github.com/nvim-tree/nvim-tree.lua/issues/3329)) ([07f541f](https://github.com/nvim-tree/nvim-tree.lua/commit/07f541fcaa4a5ae019598240362449ab7e9812b3))
* **full-name:** lower `zindex` to get consistent cursor shape ([d277467](https://github.com/nvim-tree/nvim-tree.lua/commit/d277467fc0d1d0e2bca88165a1de6b526f9f6fe8))
## [1.17.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.16.0...nvim-tree-v1.17.0) (2026-04-07) ## [1.17.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.16.0...nvim-tree-v1.17.0) (2026-04-07)

File diff suppressed because it is too large Load Diff

View File

@@ -59,13 +59,25 @@ function nvim_tree.api.fs.create(node) end
function nvim_tree.api.fs.cut(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. ---If {node} is a file it will pasted in the parent directory.
--- ---
---@param node? nvim_tree.api.Node ---@param node? nvim_tree.api.Node
function nvim_tree.api.fs.paste(node) end 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. ---Print the contents of the nvim-tree clipboard.
--- ---

View File

@@ -9,6 +9,7 @@ local find_file = require("nvim-tree.actions.finders.find-file").fn
local Class = require("nvim-tree.classic") local Class = require("nvim-tree.classic")
local DirectoryNode = require("nvim-tree.node.directory") local DirectoryNode = require("nvim-tree.node.directory")
local FileNode = require("nvim-tree.node.file")
local Node = require("nvim-tree.node") local Node = require("nvim-tree.node")
---@alias ClipboardAction "copy" | "cut" ---@alias ClipboardAction "copy" | "cut"
@@ -176,28 +177,40 @@ function Clipboard:bulk_clipboard(nodes, from, to, verb)
self.explorer.renderer:draw() self.explorer.renderer:draw()
end 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 ---Copy one or more nodes
---@param node_or_nodes Node|Node[] ---@param node_or_nodes Node|Node[]
function Clipboard:copy(node_or_nodes) 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) utils.array_remove(self.data.cut, node_or_nodes)
toggle(node_or_nodes, self.data.copy) toggle(node_or_nodes, self.data.copy)
self.explorer.renderer:draw() self.explorer.renderer:draw()
else else
self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.cut, self.data.copy, "added to") self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.cut, self.data.copy, "added to")
end end
self:copy_absolute_path(self.data.copy, { notify = false })
end end
---Cut one or more nodes ---Cut one or more nodes
---@param node_or_nodes Node|Node[] ---@param node_or_nodes Node|Node[]
function Clipboard:cut(node_or_nodes) 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) utils.array_remove(self.data.copy, node_or_nodes)
toggle(node_or_nodes, self.data.cut) toggle(node_or_nodes, self.data.cut)
self.explorer.renderer:draw() self.explorer.renderer:draw()
else else
self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.copy, self.data.cut, "cut to") self:bulk_clipboard(utils.filter_descendant_nodes(node_or_nodes), self.data.copy, self.data.cut, "cut to")
end end
self:copy_absolute_path(self.data.cut, { notify = false })
end end
---Clear clipboard for action and reload to reflect filesystem changes from paste. ---Clear clipboard for action and reload to reflect filesystem changes from paste.
@@ -304,6 +317,40 @@ function Clipboard:resolve_conflicts(conflict, destination, action, action_fn)
end) end)
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. ---Paste cut or copy with batch conflict resolution.
---@private ---@private
---@param node Node ---@param node Node
@@ -318,7 +365,9 @@ function Clipboard:do_paste(node, action, action_fn)
node = dir:last_group_node() node = dir:last_group_node()
end end
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 if #clip == 0 then
return return
end end
@@ -328,6 +377,9 @@ function Clipboard:do_paste(node, action, action_fn)
if not stats and err_name ~= "ENOENT" then if not stats and err_name ~= "ENOENT" then
log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err) log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err)
notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???")) notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???"))
if is_local == false then
self:destroy_nodes(clip)
end
return return
end end
local is_dir = stats and stats.type == "directory" local is_dir = stats and stats.type == "directory"
@@ -359,6 +411,10 @@ function Clipboard:do_paste(node, action, action_fn)
else else
self:finish_paste(action) self:finish_paste(action)
end end
if is_local == false then
self:destroy_nodes(clip)
end
end end
---@param source string ---@param source string
@@ -386,10 +442,12 @@ end
---Paste cut (if present) or copy (if present) ---Paste cut (if present) or copy (if present)
---@param node Node ---@param node Node
function Clipboard:paste(node) ---@param opts? { cut?: boolean }
if self.data.cut[1] ~= nil then 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) self:do_paste(node, "cut", do_cut)
elseif self.data.copy[1] ~= nil then else
self:do_paste(node, "copy", do_copy) self:do_paste(node, "copy", do_copy)
end end
end end
@@ -413,17 +471,15 @@ function Clipboard:print_clipboard()
end end
---@param content string ---@param content string
function Clipboard:copy_to_reg(content) ---@param message? string
-- manually firing TextYankPost does not set vim.v.event ---@param opts? { notify?: boolean }
-- workaround: create a scratch buffer with the clipboard contents and send a yank command function Clipboard:copy_to_reg(content, message, opts)
local temp_buf = vim.api.nvim_create_buf(false, true) opts = opts and opts or {}
vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content }) vim.fn.setreg(self.reg, type(content) == "table" and content or { content }, "v")
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)) if opts.notify ~= false then
notify.info(message or string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
end
end end
---@param node Node ---@param node Node
@@ -437,18 +493,20 @@ function Clipboard:copy_filename(node)
end end
end end
---@private
---@param node Node ---@param node Node
function Clipboard:copy_basename(node) ---@return string
function Clipboard:get_absolute_path(node)
if node.name == ".." then if node.name == ".." then
-- root node = self.explorer
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"))
end end
local absolute_path = node.absolute_path
return node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path
end end
---@param node Node ---@param node Node
---@return string|nil
function Clipboard:copy_path(node) function Clipboard:copy_path(node)
if node.name == ".." then if node.name == ".." then
-- root -- root
@@ -470,15 +528,44 @@ function Clipboard:copy_path(node)
end end
end end
---@param node Node ---@param node_or_nodes Node|Node[]
function Clipboard:copy_absolute_path(node) ---@param opts? { notify?: boolean }
if node.name == ".." then function Clipboard:copy_absolute_path(node_or_nodes, opts)
node = self.explorer 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 end
local absolute_path = node.absolute_path
local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path if content ~= nil then
self:copy_to_reg(content) 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_basename(node)
if node.name == ".." then
-- 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
end end
---Node is cut. Will not be copied. ---Node is cut. Will not be copied.

View File

@@ -155,14 +155,15 @@ function M.hydrate_post_setup(api)
api.filter.toggle = e_(function(e) e.filters:toggle() end) api.filter.toggle = e_(function(e) e.filters:toggle() end)
api.fs.clear_clipboard = e_(function(e) e.clipboard:clear_clipboard() 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.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.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.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.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.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 = 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.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.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) api.fs.rename = _n(function(n) require("nvim-tree.actions.fs.rename-file").rename_node(n) end)

View File

@@ -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.clear, opts("Live Filter: Clear"))
vim.keymap.set("n", "f", api.filter.live.start, opts("Live Filter: Start")) 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", "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", "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", "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", "I", api.filter.git.ignored.toggle, opts("Toggle Filter: Git Ignored"))
@@ -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.edit, opts("Open"))
vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker")) 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", "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", "P", api.node.navigate.parent, opts("Parent Directory"))
vim.keymap.set("n", "q", api.tree.close, opts("Close")) vim.keymap.set("n", "q", api.tree.close, opts("Close"))
vim.keymap.set("n", "r", api.fs.rename, opts("Rename")) vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))