@@ -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,135 @@ 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 )
204- self .explorer .renderer :draw ()
150+ --- Copy one or more nodes
151+ --- @param node_or_nodes Node | Node[]
152+ function Clipboard :copy (node_or_nodes )
153+ if node_or_nodes .is then
154+ utils .array_remove (self .data .cut , node_or_nodes )
155+ toggle (node_or_nodes , self .data .copy )
156+ self .explorer .renderer :draw ()
157+ else
158+ local nodes = utils .filter_descendant_nodes (node_or_nodes )
159+ local added = 0
160+ local removed = 0
161+ for _ , node in ipairs (nodes ) do
162+ if node .name ~= " .." then
163+ utils .array_remove (self .data .cut , node )
164+ if utils .array_remove (self .data .copy , node ) then
165+ removed = removed + 1
166+ else
167+ table.insert (self .data .copy , node )
168+ added = added + 1
169+ end
170+ end
171+ end
172+ if added > 0 then
173+ notify .info (string.format (" %d nodes added to clipboard." , added ))
174+ elseif removed > 0 then
175+ notify .info (string.format (" %d nodes removed from clipboard." , removed ))
176+ end
177+ self .explorer .renderer :draw ()
178+ end
205179end
206180
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 ()
181+ --- Cut one or more nodes
182+ --- @param node_or_nodes Node | Node[]
183+ function Clipboard :cut (node_or_nodes )
184+ if node_or_nodes .is then
185+ utils .array_remove (self .data .copy , node_or_nodes )
186+ toggle (node_or_nodes , self .data .cut )
187+ self .explorer .renderer :draw ()
188+ else
189+ local nodes = utils .filter_descendant_nodes (node_or_nodes )
190+ local added = 0
191+ local removed = 0
192+ for _ , node in ipairs (nodes ) do
193+ if node .name ~= " .." then
194+ utils .array_remove (self .data .copy , node )
195+ if utils .array_remove (self .data .cut , node ) then
196+ removed = removed + 1
197+ else
198+ table.insert (self .data .cut , node )
199+ added = added + 1
200+ end
201+ end
202+ end
203+ if added > 0 then
204+ notify .info (string.format (" %d nodes cut to clipboard." , added ))
205+ elseif removed > 0 then
206+ notify .info (string.format (" %d nodes removed from clipboard." , removed ))
207+ end
208+ self .explorer .renderer :draw ()
209+ end
213210end
214211
215- --- Paste cut or cop
212+ --- Clear clipboard for action and reload if needed.
213+ --- @private
214+ --- @param action ClipboardAction
215+ function Clipboard :finish_paste (action )
216+ self .data [action ] = {}
217+ if not self .explorer .opts .filesystem_watchers .enable then
218+ self .explorer :reload_explorer ()
219+ end
220+ end
221+
222+ --- Resolve conflicting paste items with a single batch prompt.
223+ --- @private
224+ --- @param conflict { node : Node , dest : string } []
225+ --- @param destination string
226+ --- @param action ClipboardAction
227+ --- @param action_fn ClipboardActionFn
228+ function Clipboard :resolve_conflicts (conflict , destination , action , action_fn )
229+ local names = {}
230+ for _ , item in ipairs (conflict ) do
231+ table.insert (names , item .node .name )
232+ end
233+ local prompt_select = # conflict .. " file(s) already exist: " .. table.concat (names , " , " )
234+ local prompt_input = prompt_select .. " R(ename suffix)/y/n: "
235+
236+ lib .prompt (prompt_input , prompt_select ,
237+ { " " , " y" , " n" },
238+ { " Rename (suffix)" , " Overwrite all" , " Skip all" },
239+ " nvimtree_paste_conflict" ,
240+ function (item_short )
241+ utils .clear_prompt ()
242+ if item_short == " y" then
243+ for _ , item in ipairs (conflict ) do
244+ do_paste_one (item .node .absolute_path , item .dest , action , action_fn )
245+ end
246+ self :finish_paste (action )
247+ elseif item_short == " " or item_short == " r" then
248+ vim .ui .input ({ prompt = " Suffix: " }, function (suffix )
249+ utils .clear_prompt ()
250+ if not suffix or suffix == " " then
251+ return
252+ end
253+ local still_conflict = {}
254+ for _ , item in ipairs (conflict ) do
255+ local basename = vim .fn .fnamemodify (item .node .name , " :r" )
256+ local extension = vim .fn .fnamemodify (item .node .name , " :e" )
257+ local new_name = extension ~= " " and (basename .. suffix .. " ." .. extension ) or (item .node .name .. suffix )
258+ local new_dest = utils .path_join ({ destination , new_name })
259+ local stats = vim .loop .fs_stat (new_dest )
260+ if stats then
261+ table.insert (still_conflict , { node = item .node , dest = new_dest })
262+ else
263+ do_paste_one (item .node .absolute_path , new_dest , action , action_fn )
264+ end
265+ end
266+ if # still_conflict > 0 then
267+ self :resolve_conflicts (still_conflict , destination , action , action_fn )
268+ else
269+ self :finish_paste (action )
270+ end
271+ end )
272+ else
273+ self :finish_paste (action )
274+ end
275+ end )
276+ end
277+
278+ --- Paste cut or copy with batch conflict resolution.
216279--- @private
217280--- @param node Node
218281--- @param action ClipboardAction
@@ -243,14 +306,29 @@ function Clipboard:do_paste(node, action, action_fn)
243306 destination = vim .fn .fnamemodify (destination , " :p:h" )
244307 end
245308
309+ -- Partition into conflict / no-conflict
310+ local no_conflict = {}
311+ local conflict = {}
246312 for _ , _node in ipairs (clip ) do
247313 local dest = utils .path_join ({ destination , _node .name })
248- do_single_paste (_node .absolute_path , dest , action , action_fn )
314+ local dest_stats = vim .loop .fs_stat (dest )
315+ if dest_stats then
316+ table.insert (conflict , { node = _node , dest = dest })
317+ else
318+ table.insert (no_conflict , { node = _node , dest = dest })
319+ end
249320 end
250321
251- self .data [action ] = {}
252- if not self .explorer .opts .filesystem_watchers .enable then
253- self .explorer :reload_explorer ()
322+ -- Paste non-conflicting items immediately
323+ for _ , item in ipairs (no_conflict ) do
324+ do_paste_one (item .node .absolute_path , item .dest , action , action_fn )
325+ end
326+
327+ -- Resolve conflicts in batch
328+ if # conflict > 0 then
329+ self :resolve_conflicts (conflict , destination , action , action_fn )
330+ else
331+ self :finish_paste (action )
254332 end
255333end
256334
0 commit comments