@@ -85,6 +85,76 @@ 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+ --- @class WrapNodeOrVisualOpts
119+ --- @field visual_fn ? fun ( nodes : Node[] ) bulk visual handler ; when nil , fn is called per-node
120+ --- @field filter_descendants ? boolean filter out descendant nodes in visual mode (default true )
121+
122+ --- Wrap a single-node function to be mode-dependent: in visual mode, operate
123+ --- on all nodes in the visual range; in normal mode, operate on a single node.
124+ ---
125+ --- When opts.visual_fn is provided, it receives all nodes at once (for bulk
126+ --- operations like remove/trash that need a single confirmation prompt).
127+ --- When opts.visual_fn is nil, fn is called on each node individually.
128+ ---
129+ --- @param fn fun ( node : Node , ... ): any
130+ --- @param opts ? WrapNodeOrVisualOpts
131+ --- @return fun ( node : Node ?, ... ): any
132+ local function wrap_node_or_visual (fn , opts )
133+ opts = opts or {}
134+ return function (node , ...)
135+ if is_visual_mode () then
136+ local nodes = get_visual_nodes ()
137+ if nodes then
138+ if opts .filter_descendants ~= false then
139+ nodes = utils .filter_descendant_nodes (nodes )
140+ end
141+ if opts .visual_fn then
142+ opts .visual_fn (nodes )
143+ else
144+ for _ , n in ipairs (nodes ) do
145+ fn (n , ... )
146+ end
147+ end
148+ end
149+ else
150+ node = node or wrap_explorer (" get_node_at_cursor" )()
151+ if node then
152+ return fn (node , ... )
153+ end
154+ end
155+ end
156+ end
157+
88158--- @class NodeEditOpts
89159--- @field quit_on_open boolean | nil default false
90160--- @field focus boolean | nil default true
@@ -172,18 +242,18 @@ function M.hydrate(api)
172242 api .tree .winid = view .winid
173243
174244 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 )
245+ api .fs .remove = wrap_node_or_visual (actions .fs .remove_file .fn , { visual_fn = actions . fs . remove_file . visual } )
246+ api .fs .trash = wrap_node_or_visual (actions .fs .trash .fn , { visual_fn = actions . fs . trash . visual } )
177247 api .fs .rename_node = wrap_node (actions .fs .rename_file .fn (" :t" ))
178248 api .fs .rename = wrap_node (actions .fs .rename_file .fn (" :t" ))
179249 api .fs .rename_sub = wrap_node (actions .fs .rename_file .fn (" :p:h" ))
180250 api .fs .rename_basename = wrap_node (actions .fs .rename_file .fn (" :t:r" ))
181251 api .fs .rename_full = wrap_node (actions .fs .rename_file .fn (" :p" ))
182- api .fs .cut = wrap_node (wrap_explorer_member (" clipboard" , " cut" ))
252+ api .fs .cut = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " cut" ))
183253 api .fs .paste = wrap_node (wrap_explorer_member (" clipboard" , " paste" ))
184254 api .fs .clear_clipboard = wrap_explorer_member (" clipboard" , " clear_clipboard" )
185255 api .fs .print_clipboard = wrap_explorer_member (" clipboard" , " print_clipboard" )
186- api .fs .copy .node = wrap_node (wrap_explorer_member (" clipboard" , " copy" ))
256+ api .fs .copy .node = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " copy" ))
187257 api .fs .copy .absolute_path = wrap_node (wrap_explorer_member (" clipboard" , " copy_absolute_path" ))
188258 api .fs .copy .filename = wrap_node (wrap_explorer_member (" clipboard" , " copy_filename" ))
189259 api .fs .copy .basename = wrap_node (wrap_explorer_member (" clipboard" , " copy_basename" ))
@@ -246,7 +316,7 @@ function M.hydrate(api)
246316
247317 api .marks .get = wrap_node (wrap_explorer_member (" marks" , " get" ))
248318 api .marks .list = wrap_explorer_member (" marks" , " list" )
249- api .marks .toggle = wrap_node (wrap_explorer_member (" marks" , " toggle" ))
319+ api .marks .toggle = wrap_node_or_visual (wrap_explorer_member (" marks" , " toggle" ), { filter_descendants = false } )
250320 api .marks .clear = wrap_explorer_member (" marks" , " clear" )
251321 api .marks .bulk .delete = wrap_explorer_member (" marks" , " bulk_delete" )
252322 api .marks .bulk .trash = wrap_explorer_member (" marks" , " bulk_trash" )
0 commit comments