@@ -85,6 +85,70 @@ 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+ --- Descendant nodes are always filtered out in visual mode.
121+ ---
122+ --- When visual_fn is provided, it receives all nodes at once (for bulk operations
123+ --- like remove/trash that need a single confirmation prompt).
124+ --- When visual_fn is nil, fn is called on each node individually.
125+ ---
126+ --- @param fn fun ( node : Node , ... ): any
127+ --- @param visual_fn ? fun ( nodes : Node[] ) bulk visual handler ; when nil , fn is called per-node
128+ --- @return fun ( node : Node ?, ... ): any
129+ local function wrap_node_or_visual (fn , visual_fn )
130+ return function (node , ...)
131+ if is_visual_mode () then
132+ local nodes = get_visual_nodes ()
133+ if nodes then
134+ nodes = utils .filter_descendant_nodes (nodes )
135+ if visual_fn then
136+ visual_fn (nodes )
137+ else
138+ for _ , n in ipairs (nodes ) do
139+ fn (n , ... )
140+ end
141+ end
142+ end
143+ else
144+ node = node or wrap_explorer (" get_node_at_cursor" )()
145+ if node then
146+ return fn (node , ... )
147+ end
148+ end
149+ end
150+ end
151+
88152--- @class NodeEditOpts
89153--- @field quit_on_open boolean | nil default false
90154--- @field focus boolean | nil default true
@@ -172,18 +236,18 @@ function M.hydrate(api)
172236 api .tree .winid = view .winid
173237
174238 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 )
239+ api .fs .remove = wrap_node_or_visual (actions .fs .remove_file .fn , actions . fs . remove_file . visual )
240+ api .fs .trash = wrap_node_or_visual (actions .fs .trash .fn , actions . fs . trash . visual )
177241 api .fs .rename_node = wrap_node (actions .fs .rename_file .fn (" :t" ))
178242 api .fs .rename = wrap_node (actions .fs .rename_file .fn (" :t" ))
179243 api .fs .rename_sub = wrap_node (actions .fs .rename_file .fn (" :p:h" ))
180244 api .fs .rename_basename = wrap_node (actions .fs .rename_file .fn (" :t:r" ))
181245 api .fs .rename_full = wrap_node (actions .fs .rename_file .fn (" :p" ))
182- api .fs .cut = wrap_node (wrap_explorer_member (" clipboard" , " cut" ))
246+ api .fs .cut = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " cut" ))
183247 api .fs .paste = wrap_node (wrap_explorer_member (" clipboard" , " paste" ))
184248 api .fs .clear_clipboard = wrap_explorer_member (" clipboard" , " clear_clipboard" )
185249 api .fs .print_clipboard = wrap_explorer_member (" clipboard" , " print_clipboard" )
186- api .fs .copy .node = wrap_node (wrap_explorer_member (" clipboard" , " copy" ))
250+ api .fs .copy .node = wrap_node_or_visual (wrap_explorer_member (" clipboard" , " copy" ))
187251 api .fs .copy .absolute_path = wrap_node (wrap_explorer_member (" clipboard" , " copy_absolute_path" ))
188252 api .fs .copy .filename = wrap_node (wrap_explorer_member (" clipboard" , " copy_filename" ))
189253 api .fs .copy .basename = wrap_node (wrap_explorer_member (" clipboard" , " copy_basename" ))
@@ -246,7 +310,7 @@ function M.hydrate(api)
246310
247311 api .marks .get = wrap_node (wrap_explorer_member (" marks" , " get" ))
248312 api .marks .list = wrap_explorer_member (" marks" , " list" )
249- api .marks .toggle = wrap_node (wrap_explorer_member (" marks" , " toggle" ))
313+ api .marks .toggle = wrap_node_or_visual (wrap_explorer_member (" marks" , " toggle" ))
250314 api .marks .clear = wrap_explorer_member (" marks" , " clear" )
251315 api .marks .bulk .delete = wrap_explorer_member (" marks" , " bulk_delete" )
252316 api .marks .bulk .trash = wrap_explorer_member (" marks" , " bulk_trash" )
0 commit comments