@@ -86,6 +86,76 @@ local function wrap_explorer_member(explorer_member, member_method)
8686 end
8787end
8888
89+ --- Check if the current mode is visual (v, V, or CTRL-V).
90+ --- @return boolean
91+ local function is_visual_mode ()
92+ local mode = vim .api .nvim_get_mode ().mode
93+ return mode == " v" or mode == " V" or mode == " \22 " -- \22 is CTRL-V
94+ end
95+
96+ --- Exit visual mode synchronously.
97+ local function exit_visual_mode ()
98+ local esc = vim .api .nvim_replace_termcodes (" <Esc>" , true , false , true )
99+ vim .api .nvim_feedkeys (esc , " nx" , false )
100+ end
101+
102+ --- Get the visual selection range nodes, exiting visual mode.
103+ --- @return Node[] ?
104+ local function get_visual_nodes ()
105+ local explorer = require (" nvim-tree.core" ).get_explorer ()
106+ if not explorer then
107+ return nil
108+ end
109+ local start_line = vim .fn .line (" v" )
110+ local end_line = vim .fn .line (" ." )
111+ if start_line > end_line then
112+ start_line , end_line = end_line , start_line
113+ end
114+ local nodes = explorer :get_nodes_in_range (start_line , end_line )
115+ exit_visual_mode ()
116+ return nodes
117+ end
118+
119+ --- @class WrapNodeOrVisualOpts
120+ --- @field visual_fn ? fun ( nodes : Node[] ) bulk visual handler ; when nil , fn is called per-node
121+ --- @field filter_descendants ? boolean filter out descendant nodes in visual mode (default true )
122+
123+ --- Wrap a single-node function to be mode-dependent: in visual mode, operate
124+ --- on all nodes in the visual range; in normal mode, operate on a single node.
125+ ---
126+ --- When opts.visual_fn is provided, it receives all nodes at once (for bulk
127+ --- operations like remove/trash that need a single confirmation prompt).
128+ --- When opts.visual_fn is nil, fn is called on each node individually.
129+ ---
130+ --- @param fn fun ( node : Node , ... ): any
131+ --- @param opts ? WrapNodeOrVisualOpts
132+ --- @return fun ( node : Node ?, ... ): any
133+ local function wrap_node_or_visual (fn , opts )
134+ opts = opts or {}
135+ return function (node , ...)
136+ if is_visual_mode () then
137+ local nodes = get_visual_nodes ()
138+ if nodes then
139+ if opts .filter_descendants ~= false then
140+ nodes = utils .filter_descendant_nodes (nodes )
141+ end
142+ if opts .visual_fn then
143+ opts .visual_fn (nodes )
144+ else
145+ for _ , n in ipairs (nodes ) do
146+ fn (n , ... )
147+ end
148+ end
149+ end
150+ else
151+ node = node or wrap_explorer (" get_node_at_cursor" )()
152+ if node then
153+ return fn (node , ... )
154+ end
155+ end
156+ end
157+ end
158+
89159--- @class NodeEditOpts
90160--- @field quit_on_open boolean | nil default false
91161--- @field focus boolean | nil default true
@@ -173,18 +243,18 @@ function M.hydrate(api)
173243 api .tree .winid = view .winid
174244
175245 api .fs .create = wrap_node_or_nil (actions .fs .create_file .fn )
176- api .fs .remove = wrap_node (actions .fs .remove_file .fn )
177- api .fs .trash = wrap_node (actions .fs .trash .fn )
246+ api .fs .remove = wrap_node_or_visual (actions .fs .remove_file .fn , { visual_fn = actions . fs . remove_file . visual_fn } )
247+ api .fs .trash = wrap_node_or_visual (actions .fs .trash .fn , { visual_fn = actions . fs . trash . visual_fn } )
178248 api .fs .rename_node = wrap_node (actions .fs .rename_file .fn (" :t" ))
179249 api .fs .rename = wrap_node (actions .fs .rename_file .fn (" :t" ))
180250 api .fs .rename_sub = wrap_node (actions .fs .rename_file .fn (" :p:h" ))
181251 api .fs .rename_basename = wrap_node (actions .fs .rename_file .fn (" :t:r" ))
182252 api .fs .rename_full = wrap_node (actions .fs .rename_file .fn (" :p" ))
183- api .fs .cut = wrap_node (wrap_explorer_member (" clipboard" , " cut" ))
253+ api .fs .cut = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " cut" ))
184254 api .fs .paste = wrap_node (wrap_explorer_member (" clipboard" , " paste" ))
185255 api .fs .clear_clipboard = wrap_explorer_member (" clipboard" , " clear_clipboard" )
186256 api .fs .print_clipboard = wrap_explorer_member (" clipboard" , " print_clipboard" )
187- api .fs .copy .node = wrap_node (wrap_explorer_member (" clipboard" , " copy" ))
257+ api .fs .copy .node = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " copy" ))
188258 api .fs .copy .absolute_path = wrap_node (wrap_explorer_member (" clipboard" , " copy_absolute_path" ))
189259 api .fs .copy .filename = wrap_node (wrap_explorer_member (" clipboard" , " copy_filename" ))
190260 api .fs .copy .basename = wrap_node (wrap_explorer_member (" clipboard" , " copy_basename" ))
@@ -247,7 +317,7 @@ function M.hydrate(api)
247317
248318 api .marks .get = wrap_node (wrap_explorer_member (" marks" , " get" ))
249319 api .marks .list = wrap_explorer_member (" marks" , " list" )
250- api .marks .toggle = wrap_node (wrap_explorer_member (" marks" , " toggle" ))
320+ api .marks .toggle = wrap_node_or_visual (wrap_explorer_member (" marks" , " toggle" ), { filter_descendants = false } )
251321 api .marks .clear = wrap_explorer_member (" marks" , " clear" )
252322 api .marks .bulk .delete = wrap_explorer_member (" marks" , " bulk_delete" )
253323 api .marks .bulk .trash = wrap_explorer_member (" marks" , " bulk_trash" )
0 commit comments