@@ -85,6 +85,90 @@ 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+ --- @param filter_descendants boolean ? filter out descendant nodes in visual mode
122+ --- @return fun ( node : Node ?, ... ): any
123+ local function wrap_node_or_visual (fn , filter_descendants )
124+ return function (node , ...)
125+ if is_visual_mode () then
126+ local nodes = get_visual_nodes ()
127+ if nodes then
128+ if filter_descendants then
129+ local explorer = require (" nvim-tree.core" ).get_explorer ()
130+ if explorer then
131+ nodes = explorer .marks :filter_descendant_nodes (nodes )
132+ end
133+ end
134+ for _ , n in ipairs (nodes ) do
135+ fn (n , ... )
136+ end
137+ end
138+ else
139+ node = node or wrap_explorer (" get_node_at_cursor" )()
140+ if node then
141+ return fn (node , ... )
142+ end
143+ end
144+ end
145+ end
146+
147+ --- Wrap a destructive operation to be mode-dependent: in visual mode, collect
148+ --- nodes and call a bulk method; in normal mode, call the single-node function.
149+ --- @param normal_fn fun ( node : Node ): any
150+ --- @param bulk_member string explorer member name for bulk op
151+ --- @param bulk_method string method name on member for bulk op
152+ --- @return fun ( node : Node ?): any
153+ local function wrap_node_or_visual_bulk (normal_fn , bulk_member , bulk_method )
154+ return function (node )
155+ if is_visual_mode () then
156+ local nodes = get_visual_nodes ()
157+ if nodes then
158+ local explorer = require (" nvim-tree.core" ).get_explorer ()
159+ if explorer then
160+ explorer [bulk_member ][bulk_method ](explorer [bulk_member ], nodes )
161+ end
162+ end
163+ else
164+ node = node or wrap_explorer (" get_node_at_cursor" )()
165+ if node then
166+ return normal_fn (node )
167+ end
168+ end
169+ end
170+ end
171+
88172--- @class NodeEditOpts
89173--- @field quit_on_open boolean | nil default false
90174--- @field focus boolean | nil default true
@@ -172,18 +256,18 @@ function M.hydrate(api)
172256 api .tree .winid = view .winid
173257
174258 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 )
259+ api .fs .remove = wrap_node_or_visual_bulk (actions .fs .remove_file .fn , " marks " , " bulk_delete_nodes " )
260+ api .fs .trash = wrap_node_or_visual_bulk (actions .fs .trash .fn , " marks " , " bulk_trash_nodes " )
177261 api .fs .rename_node = wrap_node (actions .fs .rename_file .fn (" :t" ))
178262 api .fs .rename = wrap_node (actions .fs .rename_file .fn (" :t" ))
179263 api .fs .rename_sub = wrap_node (actions .fs .rename_file .fn (" :p:h" ))
180264 api .fs .rename_basename = wrap_node (actions .fs .rename_file .fn (" :t:r" ))
181265 api .fs .rename_full = wrap_node (actions .fs .rename_file .fn (" :p" ))
182- api .fs .cut = wrap_node (wrap_explorer_member (" clipboard" , " cut" ))
266+ api .fs .cut = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " cut" ), true )
183267 api .fs .paste = wrap_node (wrap_explorer_member (" clipboard" , " paste" ))
184268 api .fs .clear_clipboard = wrap_explorer_member (" clipboard" , " clear_clipboard" )
185269 api .fs .print_clipboard = wrap_explorer_member (" clipboard" , " print_clipboard" )
186- api .fs .copy .node = wrap_node (wrap_explorer_member (" clipboard" , " copy" ))
270+ api .fs .copy .node = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " copy" ), true )
187271 api .fs .copy .absolute_path = wrap_node (wrap_explorer_member (" clipboard" , " copy_absolute_path" ))
188272 api .fs .copy .filename = wrap_node (wrap_explorer_member (" clipboard" , " copy_filename" ))
189273 api .fs .copy .basename = wrap_node (wrap_explorer_member (" clipboard" , " copy_basename" ))
@@ -229,8 +313,12 @@ function M.hydrate(api)
229313 api .node .expand = wrap_node (wrap_explorer (" expand_node" ))
230314 api .node .collapse = wrap_node (actions .tree .collapse .node )
231315
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 )
316+ api .node .buffer .delete = wrap_node (function (node , opts )
317+ actions .node .buffer .delete (node , opts )
318+ end )
319+ api .node .buffer .wipe = wrap_node (function (node , opts )
320+ actions .node .buffer .wipe (node , opts )
321+ end )
234322
235323 api .tree .reload_git = wrap_explorer (" reload_git" )
236324
@@ -246,7 +334,7 @@ function M.hydrate(api)
246334
247335 api .marks .get = wrap_node (wrap_explorer_member (" marks" , " get" ))
248336 api .marks .list = wrap_explorer_member (" marks" , " list" )
249- api .marks .toggle = wrap_node (wrap_explorer_member (" marks" , " toggle" ))
337+ api .marks .toggle = wrap_node_or_visual (wrap_explorer_member (" marks" , " toggle" ))
250338 api .marks .clear = wrap_explorer_member (" marks" , " clear" )
251339 api .marks .bulk .delete = wrap_explorer_member (" marks" , " bulk_delete" )
252340 api .marks .bulk .trash = wrap_explorer_member (" marks" , " bulk_trash" )
0 commit comments