@@ -85,6 +85,83 @@ local function wrap_explorer_member(explorer_member, member_method)
8585 end
8686end
8787
88+ --- Check if the current mode is visual (v, V, or CTRL-V).
89+ --- @return boolean
90+ local function is_visual_mode ()
91+ local mode = vim .api .nvim_get_mode ().mode
92+ return mode == " v" or mode == " V" or mode == " \22 " -- \22 is CTRL-V
93+ end
94+
95+ --- Exit visual mode synchronously.
96+ local function exit_visual_mode ()
97+ local esc = vim .api .nvim_replace_termcodes (" <Esc>" , true , false , true )
98+ vim .api .nvim_feedkeys (esc , " nx" , false )
99+ end
100+
101+ --- Get the visual selection range nodes, exiting visual mode.
102+ --- @return Node[] ?
103+ local function get_visual_nodes ()
104+ local explorer = require (" nvim-tree.core" ).get_explorer ()
105+ if not explorer then
106+ return nil
107+ end
108+ local start_line = vim .fn .line (" v" )
109+ local end_line = vim .fn .line (" ." )
110+ if start_line > end_line then
111+ start_line , end_line = end_line , start_line
112+ end
113+ local nodes = explorer :get_nodes_in_range (start_line , end_line )
114+ exit_visual_mode ()
115+ return nodes
116+ end
117+
118+ --- Wrap a single-node function to be mode-dependent: in visual mode, operate
119+ --- on all nodes in the visual range; in normal mode, operate on a single node.
120+ --- @param fn fun ( node : Node , ... ): any
121+ --- @return fun ( node : Node ?, ... ): any
122+ local function wrap_node_or_visual (fn )
123+ return function (node , ...)
124+ if is_visual_mode () then
125+ local nodes = get_visual_nodes ()
126+ if nodes then
127+ for _ , n in ipairs (nodes ) do
128+ fn (n )
129+ end
130+ end
131+ else
132+ node = node or wrap_explorer (" get_node_at_cursor" )()
133+ if node then
134+ return fn (node , ... )
135+ end
136+ end
137+ end
138+ end
139+
140+ --- Wrap a destructive operation to be mode-dependent: in visual mode, collect
141+ --- nodes and call a bulk method; in normal mode, call the single-node function.
142+ --- @param normal_fn fun ( node : Node ): any
143+ --- @param bulk_member string explorer member name for bulk op
144+ --- @param bulk_method string method name on member for bulk op
145+ --- @return fun ( node : Node ?): any
146+ local function wrap_node_or_visual_bulk (normal_fn , bulk_member , bulk_method )
147+ return function (node )
148+ if is_visual_mode () then
149+ local nodes = get_visual_nodes ()
150+ if nodes then
151+ local explorer = require (" nvim-tree.core" ).get_explorer ()
152+ if explorer then
153+ explorer [bulk_member ][bulk_method ](explorer [bulk_member ], nodes )
154+ end
155+ end
156+ else
157+ node = node or wrap_explorer (" get_node_at_cursor" )()
158+ if node then
159+ return normal_fn (node )
160+ end
161+ end
162+ end
163+ end
164+
88165--- @class NodeEditOpts
89166--- @field quit_on_open boolean | nil default false
90167--- @field focus boolean | nil default true
@@ -172,18 +249,18 @@ function M.hydrate(api)
172249 api .tree .winid = view .winid
173250
174251 api .fs .create = wrap_node_or_nil (actions .fs .create_file .fn )
175- api .fs .remove = wrap_node (actions .fs .remove_file .fn )
176- api .fs .trash = wrap_node (actions .fs .trash .fn )
252+ api .fs .remove = wrap_node_or_visual_bulk (actions .fs .remove_file .fn , " marks " , " bulk_delete_nodes " )
253+ api .fs .trash = wrap_node_or_visual_bulk (actions .fs .trash .fn , " marks " , " bulk_trash_nodes " )
177254 api .fs .rename_node = wrap_node (actions .fs .rename_file .fn (" :t" ))
178255 api .fs .rename = wrap_node (actions .fs .rename_file .fn (" :t" ))
179256 api .fs .rename_sub = wrap_node (actions .fs .rename_file .fn (" :p:h" ))
180257 api .fs .rename_basename = wrap_node (actions .fs .rename_file .fn (" :t:r" ))
181258 api .fs .rename_full = wrap_node (actions .fs .rename_file .fn (" :p" ))
182- api .fs .cut = wrap_node (wrap_explorer_member (" clipboard" , " cut" ))
259+ api .fs .cut = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " cut" ))
183260 api .fs .paste = wrap_node (wrap_explorer_member (" clipboard" , " paste" ))
184261 api .fs .clear_clipboard = wrap_explorer_member (" clipboard" , " clear_clipboard" )
185262 api .fs .print_clipboard = wrap_explorer_member (" clipboard" , " print_clipboard" )
186- api .fs .copy .node = wrap_node (wrap_explorer_member (" clipboard" , " copy" ))
263+ api .fs .copy .node = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " copy" ))
187264 api .fs .copy .absolute_path = wrap_node (wrap_explorer_member (" clipboard" , " copy_absolute_path" ))
188265 api .fs .copy .filename = wrap_node (wrap_explorer_member (" clipboard" , " copy_filename" ))
189266 api .fs .copy .basename = wrap_node (wrap_explorer_member (" clipboard" , " copy_basename" ))
@@ -229,8 +306,12 @@ function M.hydrate(api)
229306 api .node .expand = wrap_node (wrap_explorer (" expand_node" ))
230307 api .node .collapse = wrap_node (actions .tree .collapse .node )
231308
232- api .node .buffer .delete = wrap_node (function (node , opts ) actions .node .buffer .delete (node , opts ) end )
233- api .node .buffer .wipe = wrap_node (function (node , opts ) actions .node .buffer .wipe (node , opts ) end )
309+ api .node .buffer .delete = wrap_node (function (node , opts )
310+ actions .node .buffer .delete (node , opts )
311+ end )
312+ api .node .buffer .wipe = wrap_node (function (node , opts )
313+ actions .node .buffer .wipe (node , opts )
314+ end )
234315
235316 api .tree .reload_git = wrap_explorer (" reload_git" )
236317
@@ -246,7 +327,7 @@ function M.hydrate(api)
246327
247328 api .marks .get = wrap_node (wrap_explorer_member (" marks" , " get" ))
248329 api .marks .list = wrap_explorer_member (" marks" , " list" )
249- api .marks .toggle = wrap_node (wrap_explorer_member (" marks" , " toggle" ))
330+ api .marks .toggle = wrap_node_or_visual (wrap_explorer_member (" marks" , " toggle" ))
250331 api .marks .clear = wrap_explorer_member (" marks" , " clear" )
251332 api .marks .bulk .delete = wrap_explorer_member (" marks" , " bulk_delete" )
252333 api .marks .bulk .trash = wrap_explorer_member (" marks" , " bulk_trash" )
0 commit comments