Files
LinuxInstalls/nvim/config/nvim/lua/plugins/nvim-tree.lua
T

262 lines
12 KiB
Lua

-- Recursive copy used by the gM mapping below. Unix keeps using `cp -r`
-- (unchanged behavior); Windows has no `cp`, so it gets an equivalent
-- written against libuv. One difference there: if the destination
-- directory already exists, contents are merged into it rather than the
-- source being nested inside it, which is what `cp -r` would do.
-- Returns true on success, or false plus an error message.
local function copy_path(src, dest)
if vim.fn.has("win32") == 0 then
local result = vim.fn.system({ "cp", "-r", src, dest })
return vim.v.shell_error == 0, result
end
local uv = vim.uv or vim.loop
local function copy(s, d)
local stat = uv.fs_stat(s)
if not stat then
return false, "no such file or directory: " .. s
end
if stat.type == "directory" then
local made, mkerr = uv.fs_mkdir(d, 493) -- 0755
if not made then
local existing = uv.fs_stat(d)
if not (existing and existing.type == "directory") then
return false, mkerr
end
end
local handle = uv.fs_scandir(s)
while handle do
local name = uv.fs_scandir_next(handle)
if not name then
break
end
local ok, err = copy(s .. "/" .. name, d .. "/" .. name)
if not ok then
return false, err
end
end
return true
end
local ok, err = uv.fs_copyfile(s, d)
return ok and true or false, err
end
return copy(src, dest)
end
return {
-- NvimTree (On the right)
{
"nvim-tree/nvim-tree.lua",
dependencies = { "nvim-tree/nvim-web-devicons" },
config = function()
require("nvim-tree").setup({
view = {
side = "right",
width = 30,
},
-- Routes the paste overwrite/rename CHOICE through
-- vim.ui.select tagged with kind = "nvimtree_overwrite_rename"
-- (nvim-tree's own documented hook for this), which the gP
-- batch-paste below intercepts.
select_prompts = true,
-- Keep nvim-tree's own default mappings and add gP on top of
-- them, via the officially documented on_attach recipe --
-- rather than the TreeAttachedPost event this used before,
-- whose handler is passed the bufnr directly as a plain
-- number (confirmed from nvim-tree's own docs), not a table
-- with a .buf field -- indexing that number is what threw
-- "attempt to index local 'data' (a number value)".
on_attach = function(bufnr)
local api = require("nvim-tree.api")
-- The helper that installs the defaults has been named
-- differently across versions (api.config.mappings.
-- default_on_attach vs the older api.map.on_attach.default)
-- -- try both rather than assume one, so this doesn't
-- silently lose every default mapping on a version where
-- the other name is the real one.
if not pcall(api.config.mappings.default_on_attach, bufnr) then
pcall(api.map.on_attach.default, bufnr)
end
-- gP: paste, resolving every naming collision in this one
-- paste with a single :s///-style pattern/replacement
--
-- CAVEAT: this assumes a single-file-style overwrite/rename
-- choice per conflict. nvim-tree turns out to have a
-- SEPARATE, differently-shaped dialog specifically for
-- multi-file conflicts in one paste ("N file(s) already
-- exist" / Rename (suffix) / Overwrite all / Skip all),
-- which this doesn't detect or intercept -- so gP is only
-- confirmed reliable for a single conflicting file. For
-- "always rename every file, conflict or not" -- which is
-- what was actually being asked for -- gM below is the
-- better fit: it doesn't touch nvim-tree's paste/conflict
-- system at all, just copies marked files directly.
-- instead of nvim-tree's normal one-at-a-time
-- overwrite/rename prompt per conflicting file. Plain `p`
-- (bound above by default_on_attach) is untouched -- this
-- is a deliberately separate mapping, not a replacement.
--
-- nvim-tree doesn't expose a way to enumerate the clipboard
-- or pre-scan for collisions before pasting (only
-- print/clear it), and conflicts are resolved one at a time
-- internally as the paste runs -- there's no "here's the
-- whole batch" moment to hook into directly. This gets the
-- same practical result a different way: ask ONCE upfront
-- for the pattern, then answer nvim-tree's own conflict
-- prompts programmatically for the duration of that paste
-- using vim.fn.substitute() (real Vim regex, same engine
-- :s/// itself uses) against the filename nvim-tree
-- pre-fills as its default.
--
-- vim.ui.select/vim.ui.input are wrapped HERE, at call time
-- inside the keymap function, rather than once when this
-- plugin's config runs. nvim-tree loads eagerly at startup,
-- but dressing.nvim (which ALSO wraps these same two
-- functions, to prettify them) loads lazily on VeryLazy,
-- afterward -- wrapping once at config time would capture
-- the plain pre-dressing versions, which then get silently
-- overwritten (discarding this wrapper entirely) once
-- dressing finishes loading. Wrapping fresh on every call
-- instead always captures whatever's actually active.
vim.keymap.set('n', 'gP', function()
vim.ui.input({ prompt = "Rename pattern/replacement (Vim regex, e.g. foo/bar): " }, function(input)
if not input or input == "" then
api.fs.paste()
return
end
local pattern, replacement = input:match("^(.-)/(.*)$")
if not pattern then
vim.notify("Expected pattern/replacement, e.g. foo/bar", vim.log.levels.WARN)
return
end
local real_select = vim.ui.select
local real_input = vim.ui.input
vim.ui.select = function(items, opts, on_choice)
if opts and opts.kind == "nvimtree_overwrite_rename" then
for i, item in ipairs(items) do
if tostring(item):lower():match("rename") then
on_choice(item, i)
return
end
end
end
real_select(items, opts, on_choice)
end
vim.ui.input = function(input_opts, on_confirm)
if input_opts and input_opts.default then
local ok, new_name = pcall(vim.fn.substitute, input_opts.default, pattern, replacement, "")
on_confirm(ok and new_name ~= "" and new_name or input_opts.default)
return
end
real_input(input_opts, on_confirm)
end
-- Restored via a short defer rather than immediately
-- after api.fs.paste() returns, since it's not
-- confirmed whether paste() resolves every prompt
-- synchronously before returning to caller or defers
-- some of them -- restoring immediately risked only
-- patching the FIRST conflict in a multi-file paste and
-- silently falling back to normal prompts for the rest.
-- If that's what happens, this assumption is the next
-- thing to revisit.
api.fs.paste()
vim.defer_fn(function()
vim.ui.select = real_select
vim.ui.input = real_input
end, 2000)
end)
end, { desc = "nvim-tree: Paste (Batch Rename Regex)", buffer = bufnr, silent = true })
-- Clears the copy/cut clipboard outright. Useful if it's
-- holding a stale entry -- a file that's since been
-- renamed, moved, or deleted -- which pastes as "ENOENT: no
-- such file or directory" until cleared and re-copied fresh.
vim.keymap.set('n', 'gC', api.fs.clear_clipboard, { desc = "nvim-tree: Clear Clipboard", buffer = bufnr, silent = true })
-- gM: copy every MARKED file/folder (toggle a mark with m,
-- shown as a star) into the directory under the cursor,
-- applying a single :s///-style pattern/replacement to
-- EVERY name unconditionally -- not just on collision. This
-- is deliberately separate from nvim-tree's own copy/paste
-- clipboard (c/x/p/gP above) entirely: it reads the marked
-- list via api.marks.list() (a real, documented, stable
-- API -- unlike the clipboard, which has no equivalent way
-- to enumerate its contents) and shells out to `cp -r` to
-- do the actual copying itself, the same approach used in
-- nvim-tree's own official custom-copy recipe. Marks are
-- left as-is afterward (not auto-cleared), matching how the
-- regular clipboard also isn't cleared after a paste --
-- toggle them off individually with m if you're done with
-- them.
vim.keymap.set('n', 'gM', function()
local marks = api.marks.list()
if not marks or #marks == 0 then
vim.notify("No marked files (mark with 'm' first)", vim.log.levels.WARN)
return
end
local cursor_node = api.tree.get_node_under_cursor()
if not cursor_node then
return
end
local dest_dir = cursor_node.type == "directory" and cursor_node.absolute_path
or vim.fn.fnamemodify(cursor_node.absolute_path, ":h")
vim.ui.input({ prompt = "Rename pattern/replacement (Vim regex, e.g. Foo/Bar): " }, function(input)
if not input or input == "" then
return
end
local pattern, replacement = input:match("^(.-)/(.*)$")
if not pattern then
vim.notify("Expected pattern/replacement, e.g. Foo/Bar", vim.log.levels.WARN)
return
end
for _, node in ipairs(marks) do
if not vim.loop.fs_stat(node.absolute_path) then
-- Marks persist by path in nvim-tree's own state and
-- are NOT cleared when the underlying file/folder is
-- deleted, so a stale mark can point at a path that
-- no longer exists (e.g. you marked it, then deleted
-- it). Clean up the stale mark itself here instead
-- of attempting (and failing) a copy from it.
pcall(api.marks.toggle, node)
vim.notify("Skipped stale mark (no longer exists): " .. node.absolute_path, vim.log.levels.WARN)
else
local base = vim.fn.fnamemodify(node.absolute_path, ":t")
local ok, new_name = pcall(vim.fn.substitute, base, pattern, replacement, "")
if not ok or new_name == "" then
new_name = base
end
local dest_path = dest_dir .. "/" .. new_name
if dest_path == node.absolute_path then
-- The pattern didn't match this particular name
-- (so it came back unchanged) and the destination
-- is the same directory the file's already in --
-- that's a copy onto itself, which cp correctly
-- refuses. Skip it up front with a clearer reason
-- instead of surfacing cp's raw "same file" error.
vim.notify("Skipped " .. base .. ": pattern didn't match, and destination is the same as source", vim.log.levels.WARN)
else
local copied, result = copy_path(node.absolute_path, dest_path)
if not copied then
vim.notify("Copy failed for " .. base .. ": " .. tostring(result), vim.log.levels.ERROR)
end
end
end
end
api.tree.reload()
end)
end, { desc = "nvim-tree: Copy Marked (Regex Rename)", buffer = bufnr, silent = true })
end,
})
vim.keymap.set('n', '<leader>e', ':NvimTreeToggle<CR>', { silent = true, desc = "Toggle File Explorer" })
end,
},
}