Compare commits

..

10 Commits

Author SHA1 Message Date
Uanela Como
27aeba9b03 Merge branch 'master' into feat/rename-with-substitute 2026-07-14 12:15:43 +02:00
Igor Lacerda
cf18a662cd feat: experiment: restore nvim-tree on session load, see https://github.com/nvim-tree/nvim-tree.lua/issues/3343 (#3335)
* feat: restore buffers after loading a session

* feat(sessions): invalid json handling

* feat(sessions): switch to "persist in a file" strategy

* docs: no longer needs to toggle sessionoptions flag

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2026-07-10 11:27:15 +10:00
Alexander Courtis
a797d5e201 fix(#3346): api.tree.resize: 'configure_width' (a nil value) (#3347) 2026-07-09 10:21:38 +10:00
Alexander Courtis
35d3511e7a refactor(#3344): temporarily allow deprecated nvim_win_set_width until a replacement is provided (#3345) 2026-07-07 11:09:35 +10:00
Uanela Como
7ff7040bf6 docs: update README with cut, copy, and paste features between instances (#3342)
Update README with cut, copy, and paste features between instances
2026-07-02 11:05:47 +10:00
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
Uanela Como
0d1176b4c5 Merge branch 'master' into feat/rename-with-substitute 2026-06-22 09:23:43 +02:00
Uanela Como
85a743b8f1 Merge branch 'master' into feat/rename-with-substitute 2026-06-18 08:18:11 +02:00
uanela
68992ee244 feat: add rename with substitue command 2026-06-16 23:49:46 +02:00
14 changed files with 250 additions and 28 deletions

View File

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

View File

@@ -1,5 +1,20 @@
# 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)

View File

@@ -15,7 +15,9 @@
(Live) filtering
Cut, copy, paste, rename, delete, create
Rename, delete, create
Cut, copy, paste locally and between instances
Highly customisable

View File

@@ -1911,6 +1911,11 @@ Config: experimental *nvim-tree-config-experimental*
In the event of a problem please disable the experiment and raise an
issue.
Fields: ~
• {session_restore_nvim}? (`boolean`, default: `false`) Restore
nvim-tree buffers when restoring vim sessions
(requires 0.13+).
==============================================================================
Config: log *nvim-tree-config-log*
@@ -2216,6 +2221,7 @@ Following is the default configuration, see |nvim_tree.config| for details. >lua
persist = false,
},
experimental = {
session_restore_nvim = false
},
log = {
enable = false,

View File

@@ -9,6 +9,9 @@ error("Cannot require a meta file")
---
---@class nvim_tree.config.experimental
---
---Restore nvim-tree buffers when restoring vim sessions (requires 0.13+).
---(default: `false`)
---@field session_restore_nvim? boolean
--Example below for future reference:
--
--Buffers opened by nvim-tree will use with relative paths instead of absolute.

View File

@@ -181,7 +181,10 @@ end
---@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)
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
@@ -374,7 +377,7 @@ 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
if is_local == false then
self:destroy_nodes(clip)
end
return
@@ -399,18 +402,7 @@ function Clipboard:do_paste(node, action, action_fn)
-- Paste non-conflicting items immediately
for _, item in ipairs(no_conflict) do
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 })
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
do_paste_one(item.node.absolute_path, item.dest, action, action_fn)
end
-- Resolve conflicts in batch
@@ -420,7 +412,7 @@ function Clipboard:do_paste(node, action, action_fn)
self:finish_paste(action)
end
if ~is_local then
if is_local == false then
self:destroy_nodes(clip)
end
end

View File

@@ -3,6 +3,7 @@ local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify")
local config = require("nvim-tree.config")
local lib = require("nvim-tree.lib")
local find_file = require("nvim-tree.actions.finders.find-file").fn
@@ -47,7 +48,9 @@ end
---@param node Node
---@param to string
function M.rename(node, to)
---@param opts? { notify?: boolean }
function M.rename(node, to, opts)
opts = opts and opts or {}
local notify_from = notify.render_path(node.absolute_path)
local notify_to = notify.render_path(to)
@@ -92,7 +95,9 @@ function M.rename(node, to)
end
if not is_error then
notify.info(string.format("%s -> %s", notify_from, notify_to))
if opts.notify ~= false then
notify.info(string.format("%s -> %s", notify_from, notify_to))
end
utils.rename_loaded_buffers(node.absolute_path, to)
events._dispatch_node_renamed(node.absolute_path, to)
end
@@ -192,4 +197,24 @@ function M.rename_full(node)
prompt_to_rename(node, ":p")
end
---@param nodes Node[]
---@param old_part string
---@param new_part string
function M.bulk_rename(nodes, old_part, new_part)
local prompt_select = string.format("Substitute '%s' for '%s' in %s files?", old_part, new_part, #nodes)
lib.prompt(prompt_select .. " Y/n: ", prompt_select, ({ "", "y" }), { "No", "Yes" }, "nvimtree_bulk_rename", function(answer)
utils.clear_prompt()
if answer == "n" then return end
for _, node in ipairs(nodes) do
local new_name = node.name:gsub(old_part, new_part)
local new_full_path = vim.fn.fnamemodify(node.absolute_path, ":h") .. "/" .. new_name
M.rename(node, new_full_path, { notify = false })
end
notify.info(string.format("%s nodes substituted from %s -> %s", #nodes, old_part, new_part))
end)
end
return M

View File

@@ -1,4 +1,5 @@
local view = require("nvim-tree.view")
local view_state = require("nvim-tree.view-state")
local M = {}
@@ -7,7 +8,7 @@ local M = {}
function M.fn(opts)
if opts == nil then
-- reset to config values
view.configure_width()
view_state.configure_width()
view.resize()
return
end
@@ -16,7 +17,7 @@ function M.fn(opts)
local width_cfg = options.width
if width_cfg ~= nil then
view.configure_width(width_cfg)
view_state.configure_width(width_cfg)
view.resize()
return
end

View File

@@ -17,6 +17,28 @@ function M.global()
end,
})
if vim.fn.has("nvim-0.13") == 1 and config.g.experimental.session_restore_nvim then
vim.api.nvim_create_autocmd("SessionWritePre", { ---@diagnostic disable-line: param-type-mismatch
group = augroup_id,
callback = function()
-- We must schedule to wait until `vim.v.this_session` is properly updated
-- This prevents overwriting the data if a new session is created with a new `:mksession`
vim.schedule(require("nvim-tree.session").save)
end,
})
vim.api.nvim_create_autocmd("SessionLoadPost", {
group = augroup_id,
-- Workaround: this event fires multiple times (report upstream),
-- failure notifications could show up many times.
once = true,
callback = function()
require("nvim-tree.session").restore()
end,
})
end
if config.g.tab.sync.open then
vim.api.nvim_create_autocmd("TabEnter", {
group = augroup_id,
@@ -140,6 +162,52 @@ function M.global()
end,
})
end
vim.api.nvim_create_autocmd("CmdlineLeave", {
group = augroup_id,
pattern = ":",
callback = function(ev)
if require("nvim-tree.view").get_bufnr() == ev.buf then
local cmd = vim.fn.getcmdline()
if cmd:match("s([^%w])") then
local delimiter = cmd:match("s([^%w])")
local escaped_delim = delimiter:gsub("[%%%.%+%-%*%?%^%$%(%)%[%]]", "%%%1")
-- Looks for: s <delimiter> <old> <delimiter> <new> <delimiter or end>
local pattern = "s" .. escaped_delim .. "([^" .. escaped_delim .. "]*)" .. escaped_delim .. "([^" .. escaped_delim .. "]*)"
local old_part, new_part = cmd:match(pattern)
local core = require("nvim-tree.core")
local explorer = core.get_explorer()
local utils = require("nvim-tree.utils")
local bulk_rename = require("nvim-tree.actions.fs.rename-file").bulk_rename
local visual_marker = "'<,'>"
local nodes = cmd:sub(1, #visual_marker) == visual_marker and
utils.get_visual_nodes({ use_native = true, should_exit = false }) or
explorer and explorer:get_nodes_by_line(core.get_nodes_starting_line()) or {}
local matching_nodes = {}
for i = #nodes, 1, -1 do
local node = nodes[i]
if node and node.name:find(old_part) ~= nil then
table.insert(matching_nodes, node)
end
end
if #matching_nodes > 0 then
bulk_rename(matching_nodes, old_part, new_part)
else
require("nvim-tree.notify").notify(string.format("Not matching nodes with '%s'", old_part))
end
vim.fn.setcmdline("")
end
end
end
})
end
return M

View File

@@ -297,6 +297,7 @@ M.d = { -- config-default-start
persist = false,
},
experimental = {
session_restore_nvim = false
},
log = {
enable = false,

103
lua/nvim-tree/session.lua Normal file
View File

@@ -0,0 +1,103 @@
local notify = require("nvim-tree.notify")
local M = {}
local storepath = vim.fn.stdpath("data") .. "/nvim-tree-session.json"
local function decode_session_file()
local file = io.open(storepath, "r")
if file then
local content = file:read("*all")
file:close()
if content and content ~= "" then
return vim.json.decode(content)
end
end
-- Notifying on failure could be noisy, as it'd happen if the file simply did not exist
end
local function write_session_file(session_file_content)
local file, errmsg = io.open(storepath, "w")
if file then
file:write(vim.json.encode(session_file_content))
file:close()
else
notify.warn(string.format("Invalid session file (%s): %s", storepath, errmsg))
end
end
function M.save()
-- We either always save the session data, which ends up populating the storepath with empty data
-- Or we gate by the tree's visibility, which could create "out of sync" scenario:
-- if the tree is initially open and the session is saved, it won't be overridden later,
-- if the tree is no longer visible. However, when restoring, this is harmless.
if require("nvim-tree.api").tree.is_visible({ any_tabpage = true }) then
local ok, session_file_content = pcall(decode_session_file)
if not ok then
notify.warn(string.format("Failed to decode session file (%s): %s", storepath, session_file_content))
return
end
-- might be nil at this point
session_file_content = session_file_content or {}
local cwd = require("nvim-tree.core").get_cwd()
session_file_content[vim.v.this_session] = { cwd = cwd }
local ok2, err = pcall(write_session_file, session_file_content)
if not ok2 then
notify.warn(string.format("Failed to write session file (%s): %s", storepath, err))
end
end
end
function M.restore()
local api = require("nvim-tree.api").tree
---@type table<integer,boolean>
local tabs = {}
local ok, session_file_content = pcall(decode_session_file)
-- Failing to restore cwd is not fatal, we can continue
if not ok then
notify.warn(string.format("Failed to decode session file (%s): %s", storepath, session_file_content))
end
local path = session_file_content and session_file_content[vim.v.this_session] and session_file_content[vim.v.this_session].cwd or nil
-- Save tabs with leftover nvim-tree buffers
for _, tab in ipairs(vim.api.nvim_list_tabpages()) do
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab)) do
local buf = vim.api.nvim_win_get_buf(win)
if api.is_tree_buf(buf) then
tabs[tab] = true
end
end
end
-- The new window may inherit options from the current one if we don't schedule
vim.schedule(function()
api.close_in_all_tabs()
for tab, _ in pairs(tabs) do
if vim.api.nvim_tabpage_is_valid(tab) then
local win = vim.api.nvim_tabpage_get_win(tab)
-- Invoke `open` from each tab's current window to prevent having to switch tabs
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_call(win, function()
api.open({ path = path })
end)
end
end
end
end)
end
return M

View File

@@ -527,19 +527,25 @@ function M.exit_visual_mode()
end
---Get the visual selection range nodes, exiting visual mode.
---@param opts? { should_exit?: boolean, use_native?: boolean }
---@return Node[]?
function M.get_visual_nodes()
function M.get_visual_nodes(opts)
opts = opts and opts or {}
local explorer = require("nvim-tree.core").get_explorer()
if not explorer then
return nil
end
local start_line = vim.fn.line("v")
local end_line = vim.fn.line(".")
local start_line = vim.fn.line(opts.use_native == true and "'<" or "v")
local end_line = vim.fn.line(opts.use_native == true and "'>" or ".")
if start_line > end_line then
start_line, end_line = end_line, start_line
end
local nodes = explorer:get_nodes_in_range(start_line, end_line)
M.exit_visual_mode()
if opts.should_exit ~= false then
M.exit_visual_mode()
end
return nodes
end

View File

@@ -71,7 +71,7 @@ end
---Configure width-related config
---@param width string|function|number|table|nil
local function configure_width(width)
function M.configure_width(width)
if type(width) == "table" then
M.Active.adaptive_size = true
M.Active.width = width.min or DEFAULT_MIN_WIDTH
@@ -101,7 +101,7 @@ function M.initialize()
M.Active.winopts.relativenumber = config.g.view.relativenumber
M.Active.winopts.signcolumn = config.g.view.signcolumn
configure_width(config.g.view.width)
M.configure_width(config.g.view.width)
M.Active.initial_width = M.get_width()
end

View File

@@ -298,7 +298,7 @@ function M.resize(size)
local new_size = view_state.get_width()
if new_size ~= vim.api.nvim_win_get_width(winnr) then
vim.api.nvim_win_set_width(winnr, new_size)
vim.api.nvim_win_set_width(winnr, new_size) ---@diagnostic disable-line: deprecated see #3344
if not config.g.view.preserve_window_proportions then
vim.cmd(":wincmd =")
end