diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 8d03082342d..baae74b0003 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -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()| @@ -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()| @@ -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()| @@ -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")) @@ -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")) @@ -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 @@ -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. diff --git a/lua/nvim-tree/_meta/api/fs.lua b/lua/nvim-tree/_meta/api/fs.lua index dfab8610434..d58cc8248d2 100644 --- a/lua/nvim-tree/_meta/api/fs.lua +++ b/lua/nvim-tree/_meta/api/fs.lua @@ -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 + +--- +---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. --- diff --git a/lua/nvim-tree/_meta/config/actions.lua b/lua/nvim-tree/_meta/config/actions.lua index 012f4c364d9..8b9891f66d8 100644 --- a/lua/nvim-tree/_meta/config/actions.lua +++ b/lua/nvim-tree/_meta/config/actions.lua @@ -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 - - --- vim [current-directory] behaviour ---@class nvim_tree.config.actions.change_dir --- diff --git a/lua/nvim-tree/actions/fs/clipboard.lua b/lua/nvim-tree/actions/fs/clipboard.lua index e3abe5efd1f..bb4234498e2 100644 --- a/lua/nvim-tree/actions/fs/clipboard.lua +++ b/lua/nvim-tree/actions/fs/clipboard.lua @@ -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" @@ -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. @@ -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 @@ -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 @@ -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 @@ -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, + } + + 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 @@ -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. diff --git a/lua/nvim-tree/api/impl.lua b/lua/nvim-tree/api/impl.lua index f17fa4ee825..4f697713603 100644 --- a/lua/nvim-tree/api/impl.lua +++ b/lua/nvim-tree/api/impl.lua @@ -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) diff --git a/lua/nvim-tree/keymap.lua b/lua/nvim-tree/keymap.lua index 044bf6c9a9b..3250470db70 100644 --- a/lua/nvim-tree/keymap.lua +++ b/lua/nvim-tree/keymap.lua @@ -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")) @@ -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.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")) @@ -112,8 +113,8 @@ function M.on_attach_default(bufnr) 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