@@ -9,6 +9,7 @@ local find_file = require("nvim-tree.actions.finders.find-file").fn
99
1010local Class = require (" nvim-tree.classic" )
1111local DirectoryNode = require (" nvim-tree.node.directory" )
12+ local Node = require (" nvim-tree.node" )
1213
1314--- @alias ClipboardAction " copy" | " cut"
1415--- @alias ClipboardData table<ClipboardAction , Node[]>
@@ -108,67 +109,18 @@ local function do_copy(source, destination)
108109 return true
109110end
110111
112+ --- Paste a single item with no conflict handling.
111113--- @param source string
112114--- @param dest string
113115--- @param action ClipboardAction
114116--- @param action_fn ClipboardActionFn
115- --- @return boolean | nil -- success
116- --- @return string | nil -- error message
117- local function do_single_paste (source , dest , action , action_fn )
118- local notify_source = notify .render_path (source )
119-
120- log .line (" copy_paste" , " do_single_paste '%s' -> '%s'" , source , dest )
121-
122- local dest_stats , err , err_name = vim .loop .fs_stat (dest )
123- if not dest_stats and err_name ~= " ENOENT" then
124- notify .error (" Could not " .. action .. " " .. notify_source .. " - " .. (err or " ???" ))
125- return false , err
126- end
127-
128- local function on_process ()
129- local success , error = action_fn (source , dest )
130- if not success then
131- notify .error (" Could not " .. action .. " " .. notify_source .. " - " .. (error or " ???" ))
132- return false , error
133- end
134-
135- find_file (utils .path_remove_trailing (dest ))
136- end
137-
138- if dest_stats then
139- local input_opts = {
140- prompt = " Rename to " ,
141- default = dest ,
142- completion = " dir" ,
143- }
144-
145- if source == dest then
146- vim .ui .input (input_opts , function (new_dest )
147- utils .clear_prompt ()
148- if new_dest then
149- do_single_paste (source , new_dest , action , action_fn )
150- end
151- end )
152- else
153- local prompt_select = " Overwrite " .. dest .. " ?"
154- local prompt_input = prompt_select .. " R(ename)/y/n: "
155- lib .prompt (prompt_input , prompt_select , { " " , " y" , " n" }, { " Rename" , " Yes" , " No" }, " nvimtree_overwrite_rename" , function (item_short )
156- utils .clear_prompt ()
157- if item_short == " y" then
158- on_process ()
159- elseif item_short == " " or item_short == " r" then
160- vim .ui .input (input_opts , function (new_dest )
161- utils .clear_prompt ()
162- if new_dest then
163- do_single_paste (source , new_dest , action , action_fn )
164- end
165- end )
166- end
167- end )
168- end
169- else
170- on_process ()
117+ local function do_paste_one (source , dest , action , action_fn )
118+ log .line (" copy_paste" , " do_paste_one '%s' -> '%s'" , source , dest )
119+ local success , err = action_fn (source , dest )
120+ if not success then
121+ notify .error (" Could not " .. action .. " " .. notify .render_path (source ) .. " - " .. (err or " ???" ))
171122 end
123+ find_file (utils .path_remove_trailing (dest ))
172124end
173125
174126--- @param node Node
@@ -196,23 +148,119 @@ function Clipboard:clear_clipboard()
196148 self .explorer .renderer :draw ()
197149end
198150
199- --- Copy one node
200- --- @param node Node
201- function Clipboard :copy (node )
202- utils .array_remove (self .data .cut , node )
203- toggle (node , self .data .copy )
151+ --- Bulk add/remove nodes to/from a clipboard list.
152+ --- @private
153+ --- @param nodes Node[] filtered nodes to operate on
154+ --- @param from Node[] list to remove from (the opposite clipboard )
155+ --- @param to Node[] list to add to
156+ --- @param verb string notification verb (" added to" or " cut to" )
157+ function Clipboard :bulk_clipboard (nodes , from , to , verb )
158+ local added = 0
159+ local removed = 0
160+ for _ , node in ipairs (nodes ) do
161+ if node .name ~= " .." then
162+ utils .array_remove (from , node )
163+ if utils .array_remove (to , node ) then
164+ removed = removed + 1
165+ else
166+ table.insert (to , node )
167+ added = added + 1
168+ end
169+ end
170+ end
171+ if added > 0 then
172+ notify .info (string.format (" %d nodes %s clipboard." , added , verb ))
173+ elseif removed > 0 then
174+ notify .info (string.format (" %d nodes removed from clipboard." , removed ))
175+ end
204176 self .explorer .renderer :draw ()
205177end
206178
207- --- Cut one node
208- --- @param node Node
209- function Clipboard :cut (node )
210- utils .array_remove (self .data .copy , node )
211- toggle (node , self .data .cut )
212- self .explorer .renderer :draw ()
179+ --- Copy one or more nodes
180+ --- @param node_or_nodes Node | Node[]
181+ function Clipboard :copy (node_or_nodes )
182+ if type (node_or_nodes ) == " table" and node_or_nodes .is and node_or_nodes :is (Node ) then
183+ utils .array_remove (self .data .cut , node_or_nodes )
184+ toggle (node_or_nodes , self .data .copy )
185+ self .explorer .renderer :draw ()
186+ else
187+ self :bulk_clipboard (utils .filter_descendant_nodes (node_or_nodes ), self .data .cut , self .data .copy , " added to" )
188+ end
213189end
214190
215- --- Paste cut or cop
191+ --- Cut one or more nodes
192+ --- @param node_or_nodes Node | Node[]
193+ function Clipboard :cut (node_or_nodes )
194+ if type (node_or_nodes ) == " table" and node_or_nodes .is and node_or_nodes :is (Node ) then
195+ utils .array_remove (self .data .copy , node_or_nodes )
196+ toggle (node_or_nodes , self .data .cut )
197+ self .explorer .renderer :draw ()
198+ else
199+ self :bulk_clipboard (utils .filter_descendant_nodes (node_or_nodes ), self .data .copy , self .data .cut , " cut to" )
200+ end
201+ end
202+
203+ --- Clear clipboard for action and reload to reflect filesystem changes from paste.
204+ --- @private
205+ --- @param action ClipboardAction
206+ function Clipboard :finish_paste (action )
207+ self .data [action ] = {}
208+ self .explorer :reload_explorer ()
209+ end
210+
211+ --- Resolve conflicting paste items with a single batch prompt.
212+ --- @private
213+ --- @param conflict { node : Node , dest : string } []
214+ --- @param destination string
215+ --- @param action ClipboardAction
216+ --- @param action_fn ClipboardActionFn
217+ function Clipboard :resolve_conflicts (conflict , destination , action , action_fn )
218+ local prompt_select = # conflict .. " file(s) already exist"
219+ local prompt_input = prompt_select .. " . R(ename suffix)/y/n: "
220+
221+ lib .prompt (prompt_input , prompt_select ,
222+ { " " , " y" , " n" },
223+ { " Rename (suffix)" , " Overwrite all" , " Skip all" },
224+ " nvimtree_paste_conflict" ,
225+ function (item_short )
226+ utils .clear_prompt ()
227+ if item_short == " y" then
228+ for _ , item in ipairs (conflict ) do
229+ do_paste_one (item .node .absolute_path , item .dest , action , action_fn )
230+ end
231+ self :finish_paste (action )
232+ elseif item_short == " " or item_short == " r" then
233+ vim .ui .input ({ prompt = " Suffix: " }, function (suffix )
234+ utils .clear_prompt ()
235+ if not suffix or suffix == " " then
236+ return
237+ end
238+ local still_conflict = {}
239+ for _ , item in ipairs (conflict ) do
240+ local basename = vim .fn .fnamemodify (item .node .name , " :r" )
241+ local extension = vim .fn .fnamemodify (item .node .name , " :e" )
242+ local new_name = extension ~= " " and (basename .. suffix .. " ." .. extension ) or (item .node .name .. suffix )
243+ local new_dest = utils .path_join ({ destination , new_name })
244+ local stats = vim .loop .fs_stat (new_dest )
245+ if stats then
246+ table.insert (still_conflict , { node = item .node , dest = new_dest })
247+ else
248+ do_paste_one (item .node .absolute_path , new_dest , action , action_fn )
249+ end
250+ end
251+ if # still_conflict > 0 then
252+ self :resolve_conflicts (still_conflict , destination , action , action_fn )
253+ else
254+ self :finish_paste (action )
255+ end
256+ end )
257+ else
258+ self :finish_paste (action )
259+ end
260+ end )
261+ end
262+
263+ --- Paste cut or copy with batch conflict resolution.
216264--- @private
217265--- @param node Node
218266--- @param action ClipboardAction
@@ -243,14 +291,29 @@ function Clipboard:do_paste(node, action, action_fn)
243291 destination = vim .fn .fnamemodify (destination , " :p:h" )
244292 end
245293
294+ -- Partition into conflict / no-conflict
295+ local no_conflict = {}
296+ local conflict = {}
246297 for _ , _node in ipairs (clip ) do
247298 local dest = utils .path_join ({ destination , _node .name })
248- do_single_paste (_node .absolute_path , dest , action , action_fn )
299+ local dest_stats = vim .loop .fs_stat (dest )
300+ if dest_stats then
301+ table.insert (conflict , { node = _node , dest = dest })
302+ else
303+ table.insert (no_conflict , { node = _node , dest = dest })
304+ end
249305 end
250306
251- self .data [action ] = {}
252- if not self .explorer .opts .filesystem_watchers .enable then
253- self .explorer :reload_explorer ()
307+ -- Paste non-conflicting items immediately
308+ for _ , item in ipairs (no_conflict ) do
309+ do_paste_one (item .node .absolute_path , item .dest , action , action_fn )
310+ end
311+
312+ -- Resolve conflicts in batch
313+ if # conflict > 0 then
314+ self :resolve_conflicts (conflict , destination , action , action_fn )
315+ else
316+ self :finish_paste (action )
254317 end
255318end
256319
0 commit comments