@@ -108,67 +108,18 @@ local function do_copy(source, destination)
108108 return true
109109end
110110
111+ --- Paste a single item with no conflict handling.
111112--- @param source string
112113--- @param dest string
113114--- @param action ClipboardAction
114115--- @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 ()
116+ local function do_paste_one (source , dest , action , action_fn )
117+ log .line (" copy_paste" , " do_paste_one '%s' -> '%s'" , source , dest )
118+ local success , err = action_fn (source , dest )
119+ if not success then
120+ notify .error (" Could not " .. action .. " " .. notify .render_path (source ) .. " - " .. (err or " ???" ))
171121 end
122+ find_file (utils .path_remove_trailing (dest ))
172123end
173124
174125--- @param node Node
@@ -196,23 +147,122 @@ function Clipboard:clear_clipboard()
196147 self .explorer .renderer :draw ()
197148end
198149
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 )
150+ --- Bulk add/remove nodes to/from a clipboard list.
151+ --- @private
152+ --- @param nodes Node[] filtered nodes to operate on
153+ --- @param from Node[] list to remove from (the opposite clipboard )
154+ --- @param to Node[] list to add to
155+ --- @param verb string notification verb (" added to" or " cut to" )
156+ function Clipboard :bulk_clipboard (nodes , from , to , verb )
157+ local added = 0
158+ local removed = 0
159+ for _ , node in ipairs (nodes ) do
160+ if node .name ~= " .." then
161+ utils .array_remove (from , node )
162+ if utils .array_remove (to , node ) then
163+ removed = removed + 1
164+ else
165+ table.insert (to , node )
166+ added = added + 1
167+ end
168+ end
169+ end
170+ if added > 0 then
171+ notify .info (string.format (" %d nodes %s clipboard." , added , verb ))
172+ elseif removed > 0 then
173+ notify .info (string.format (" %d nodes removed from clipboard." , removed ))
174+ end
204175 self .explorer .renderer :draw ()
205176end
206177
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 )
178+ --- Copy one or more nodes
179+ --- @param node_or_nodes Node | Node[]
180+ function Clipboard :copy (node_or_nodes )
181+ if node_or_nodes .is then
182+ utils .array_remove (self .data .cut , node_or_nodes )
183+ toggle (node_or_nodes , self .data .copy )
184+ self .explorer .renderer :draw ()
185+ else
186+ self :bulk_clipboard (utils .filter_descendant_nodes (node_or_nodes ), self .data .cut , self .data .copy , " added to" )
187+ end
188+ end
189+
190+ --- Cut one or more nodes
191+ --- @param node_or_nodes Node | Node[]
192+ function Clipboard :cut (node_or_nodes )
193+ if node_or_nodes .is then
194+ utils .array_remove (self .data .copy , node_or_nodes )
195+ toggle (node_or_nodes , self .data .cut )
196+ self .explorer .renderer :draw ()
197+ else
198+ self :bulk_clipboard (utils .filter_descendant_nodes (node_or_nodes ), self .data .copy , self .data .cut , " cut to" )
199+ end
200+ end
201+
202+ --- Clear clipboard for action and reload if needed.
203+ --- @private
204+ --- @param action ClipboardAction
205+ function Clipboard :finish_paste (action )
206+ self .data [action ] = {}
207+ if not self .explorer .opts .filesystem_watchers .enable then
208+ self .explorer :reload_explorer ()
209+ end
212210 self .explorer .renderer :draw ()
213211end
214212
215- --- Paste cut or cop
213+ --- Resolve conflicting paste items with a single batch prompt.
214+ --- @private
215+ --- @param conflict { node : Node , dest : string } []
216+ --- @param destination string
217+ --- @param action ClipboardAction
218+ --- @param action_fn ClipboardActionFn
219+ function Clipboard :resolve_conflicts (conflict , destination , action , action_fn )
220+ local prompt_select = # conflict .. " file(s) already exist"
221+ local prompt_input = prompt_select .. " . R(ename suffix)/y/n: "
222+
223+ lib .prompt (prompt_input , prompt_select ,
224+ { " " , " y" , " n" },
225+ { " Rename (suffix)" , " Overwrite all" , " Skip all" },
226+ " nvimtree_paste_conflict" ,
227+ function (item_short )
228+ utils .clear_prompt ()
229+ if item_short == " y" then
230+ for _ , item in ipairs (conflict ) do
231+ do_paste_one (item .node .absolute_path , item .dest , action , action_fn )
232+ end
233+ self :finish_paste (action )
234+ elseif item_short == " " or item_short == " r" then
235+ vim .ui .input ({ prompt = " Suffix: " }, function (suffix )
236+ utils .clear_prompt ()
237+ if not suffix or suffix == " " then
238+ return
239+ end
240+ local still_conflict = {}
241+ for _ , item in ipairs (conflict ) do
242+ local basename = vim .fn .fnamemodify (item .node .name , " :r" )
243+ local extension = vim .fn .fnamemodify (item .node .name , " :e" )
244+ local new_name = extension ~= " " and (basename .. suffix .. " ." .. extension ) or (item .node .name .. suffix )
245+ local new_dest = utils .path_join ({ destination , new_name })
246+ local stats = vim .loop .fs_stat (new_dest )
247+ if stats then
248+ table.insert (still_conflict , { node = item .node , dest = new_dest })
249+ else
250+ do_paste_one (item .node .absolute_path , new_dest , action , action_fn )
251+ end
252+ end
253+ if # still_conflict > 0 then
254+ self :resolve_conflicts (still_conflict , destination , action , action_fn )
255+ else
256+ self :finish_paste (action )
257+ end
258+ end )
259+ else
260+ self :finish_paste (action )
261+ end
262+ end )
263+ end
264+
265+ --- Paste cut or copy with batch conflict resolution.
216266--- @private
217267--- @param node Node
218268--- @param action ClipboardAction
@@ -243,14 +293,29 @@ function Clipboard:do_paste(node, action, action_fn)
243293 destination = vim .fn .fnamemodify (destination , " :p:h" )
244294 end
245295
296+ -- Partition into conflict / no-conflict
297+ local no_conflict = {}
298+ local conflict = {}
246299 for _ , _node in ipairs (clip ) do
247300 local dest = utils .path_join ({ destination , _node .name })
248- do_single_paste (_node .absolute_path , dest , action , action_fn )
301+ local dest_stats = vim .loop .fs_stat (dest )
302+ if dest_stats then
303+ table.insert (conflict , { node = _node , dest = dest })
304+ else
305+ table.insert (no_conflict , { node = _node , dest = dest })
306+ end
249307 end
250308
251- self .data [action ] = {}
252- if not self .explorer .opts .filesystem_watchers .enable then
253- self .explorer :reload_explorer ()
309+ -- Paste non-conflicting items immediately
310+ for _ , item in ipairs (no_conflict ) do
311+ do_paste_one (item .node .absolute_path , item .dest , action , action_fn )
312+ end
313+
314+ -- Resolve conflicts in batch
315+ if # conflict > 0 then
316+ self :resolve_conflicts (conflict , destination , action , action_fn )
317+ else
318+ self :finish_paste (action )
254319 end
255320end
256321
0 commit comments