Skip to content

Commit 8fdb815

Browse files
authored
feat: improve visual selection handling in file tree buffers (#132)
# Improved Visual Selection Support for Tree Buffers This PR enhances the file selection mechanism in tree-based file explorers (neo-tree, nvim-tree, mini.files) when using visual mode. The changes: - Add dedicated visual selection handling for tree buffers that accurately captures files in visual selections - Implement a fallback mechanism that tries visual selection first, then falls back to regular selection methods - Add detailed logging for neo-tree integration to help diagnose selection issues - Support directory nodes in visual selections for neo-tree - Add unit tests to verify visual selection handling in tree buffers These improvements make it more intuitive to select multiple files in file explorers using visual mode (V), similar to how neo-tree's built-in commands work with visual selections.
1 parent ef9cca1 commit 8fdb815

File tree

4 files changed

+317
-7
lines changed

4 files changed

+317
-7
lines changed

lua/claudecode/init.lua

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -691,22 +691,27 @@ function M._create_commands()
691691

692692
if is_tree_buffer then
693693
local integrations = require("claudecode.integrations")
694+
local visual_cmd_module = require("claudecode.visual_commands")
694695
local files, error
695696

696-
-- For mini.files, try to get the range from visual marks
697+
-- For mini.files, try to get the range from visual marks for accuracy
697698
if current_ft == "minifiles" or string.match(current_bufname, "minifiles://") then
698699
local start_line = vim.fn.line("'<")
699700
local end_line = vim.fn.line("'>")
700701

701702
if start_line > 0 and end_line > 0 and start_line <= end_line then
702-
-- Use range-based selection for mini.files
703703
files, error = integrations._get_mini_files_selection_with_range(start_line, end_line)
704704
else
705-
-- Fall back to regular method
706-
files, error = integrations.get_selected_files_from_tree()
705+
-- If range invalid, try visual selection fallback (uses pre-captured visual_data)
706+
files, error = visual_cmd_module.get_files_from_visual_selection(visual_data)
707707
end
708708
else
709-
files, error = integrations.get_selected_files_from_tree()
709+
-- Use visual selection-aware extraction for tree buffers (neo-tree, nvim-tree, oil)
710+
files, error = visual_cmd_module.get_files_from_visual_selection(visual_data)
711+
if (not files or #files == 0) and not error then
712+
-- Fallback: try generic selection if visual data was unavailable
713+
files, error = integrations.get_selected_files_from_tree()
714+
end
710715
end
711716

712717
if error then

lua/claudecode/integrations.lua

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
--- Handles detection and selection of files from nvim-tree, neo-tree, mini.files, and oil.nvim
33
---@module 'claudecode.integrations'
44
local M = {}
5+
local logger = require("claudecode.logger")
56

67
---Get selected files from the current tree explorer
78
---@return table|nil files List of file paths, or nil if error
@@ -75,22 +76,33 @@ end
7576
function M._get_neotree_selection()
7677
local success, manager = pcall(require, "neo-tree.sources.manager")
7778
if not success then
79+
logger.debug("integrations/neotree", "neo-tree not available (require failed)")
7880
return {}, "neo-tree not available"
7981
end
8082

8183
local state = manager.get_state("filesystem")
8284
if not state then
85+
logger.debug("integrations/neotree", "filesystem state not available from manager")
8386
return {}, "neo-tree filesystem state not available"
8487
end
8588

8689
local files = {}
8790

8891
-- Use neo-tree's own visual selection method (like their copy/paste feature)
8992
local mode = vim.fn.mode()
93+
local current_win = vim.api.nvim_get_current_win()
94+
logger.debug(
95+
"integrations/neotree",
96+
"begin selection",
97+
"mode=",
98+
mode,
99+
"current_win=",
100+
current_win,
101+
"state.winid=",
102+
tostring(state.winid)
103+
)
90104

91105
if mode == "V" or mode == "v" or mode == "\22" then
92-
local current_win = vim.api.nvim_get_current_win()
93-
94106
if state.winid and state.winid == current_win then
95107
-- Use neo-tree's exact method to get visual range (from their get_selected_nodes implementation)
96108
local start_pos = vim.fn.getpos("'<")[2]
@@ -113,6 +125,8 @@ function M._get_neotree_selection()
113125
start_pos, end_pos = end_pos, start_pos
114126
end
115127

128+
logger.debug("integrations/neotree", "visual selection range", start_pos, "to", end_pos)
129+
116130
local selected_nodes = {}
117131

118132
for line = start_pos, end_pos do
@@ -121,22 +135,59 @@ function M._get_neotree_selection()
121135
-- Add validation for node types before adding to selection
122136
if node.type and node.type ~= "message" then
123137
table.insert(selected_nodes, node)
138+
local depth = (node.get_depth and node:get_depth()) and node:get_depth() or 0
139+
logger.debug(
140+
"integrations/neotree",
141+
"line",
142+
line,
143+
"node type=",
144+
tostring(node.type),
145+
"depth=",
146+
depth,
147+
"path=",
148+
tostring(node.path)
149+
)
150+
else
151+
logger.debug("integrations/neotree", "line", line, "node rejected (type)", tostring(node and node.type))
124152
end
153+
else
154+
logger.debug("integrations/neotree", "line", line, "no node returned from state.tree:get_node")
125155
end
126156
end
127157

158+
logger.debug("integrations/neotree", "selected_nodes count=", #selected_nodes)
159+
128160
for _, node in ipairs(selected_nodes) do
129161
-- Enhanced validation: check for file type and valid path
130162
if node.type == "file" and node.path and node.path ~= "" then
131163
-- Additional check: ensure it's not a root node (depth protection)
132164
local depth = (node.get_depth and node:get_depth()) and node:get_depth() or 0
133165
if depth > 1 then
134166
table.insert(files, node.path)
167+
logger.debug("integrations/neotree", "accepted file", node.path)
168+
else
169+
logger.debug("integrations/neotree", "rejected file (depth<=1)", node.path)
135170
end
171+
elseif node.type == "directory" and node.path and node.path ~= "" then
172+
local depth = (node.get_depth and node:get_depth()) and node:get_depth() or 0
173+
if depth > 1 then
174+
table.insert(files, node.path)
175+
logger.debug("integrations/neotree", "accepted directory", node.path)
176+
else
177+
logger.debug("integrations/neotree", "rejected directory (depth<=1)", node.path)
178+
end
179+
else
180+
logger.debug(
181+
"integrations/neotree",
182+
"rejected node (missing path or unsupported type)",
183+
tostring(node and node.type),
184+
tostring(node and node.path)
185+
)
136186
end
137187
end
138188

139189
if #files > 0 then
190+
logger.debug("integrations/neotree", "files from visual selection:", files)
140191
return files, nil
141192
end
142193
end
@@ -154,13 +205,23 @@ function M._get_neotree_selection()
154205
end
155206

156207
if selection and #selection > 0 then
208+
logger.debug("integrations/neotree", "using state selection count=", #selection)
157209
for _, node in ipairs(selection) do
158210
if node.type == "file" and node.path then
159211
table.insert(files, node.path)
212+
logger.debug("integrations/neotree", "accepted file from state selection", node.path)
213+
else
214+
logger.debug(
215+
"integrations/neotree",
216+
"ignored non-file in state selection",
217+
tostring(node and node.type),
218+
tostring(node and node.path)
219+
)
160220
end
161221
end
162222

163223
if #files > 0 then
224+
logger.debug("integrations/neotree", "files from state selection:", files)
164225
return files, nil
165226
end
166227
end
@@ -170,6 +231,14 @@ function M._get_neotree_selection()
170231
local node = state.tree:get_node()
171232

172233
if node then
234+
logger.debug(
235+
"integrations/neotree",
236+
"fallback single node",
237+
"type=",
238+
tostring(node.type),
239+
"path=",
240+
tostring(node.path)
241+
)
173242
if node.type == "file" and node.path then
174243
return { node.path }, nil
175244
elseif node.type == "directory" and node.path then
@@ -178,6 +247,7 @@ function M._get_neotree_selection()
178247
end
179248
end
180249

250+
logger.debug("integrations/neotree", "no file found under cursor/selection")
181251
return {}, "No file found under cursor"
182252
end
183253

lua/claudecode/visual_commands.lua

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
---Implements neo-tree-style visual mode exit and command processing
33
---@module 'claudecode.visual_commands'
44
local M = {}
5+
local logger = require("claudecode.logger")
56

67
---Get current vim mode with fallback for test environments
78
---@param full_mode? boolean Whether to get full mode info (passed to vim.fn.mode)
@@ -38,6 +39,17 @@ function M.exit_visual_and_schedule(callback, ...)
3839
-- Capture visual selection data BEFORE exiting visual mode
3940
local visual_data = M.capture_visual_selection_data()
4041

42+
if visual_data and visual_data.tree_type == "neo-tree" then
43+
logger.debug(
44+
"visual_commands/neotree",
45+
"captured visual before exit",
46+
"range=",
47+
visual_data.start_pos,
48+
"to",
49+
visual_data.end_pos
50+
)
51+
end
52+
4153
pcall(function()
4254
vim.api.nvim_feedkeys(ESC_KEY, "i", true)
4355
end)
@@ -162,8 +174,17 @@ function M.get_tree_state()
162174

163175
-- Validate we're in the correct neo-tree window
164176
if state.winid and state.winid == current_win then
177+
logger.debug("visual_commands/neotree", "tree state detected for current window", "win=", current_win)
165178
return state, "neo-tree"
166179
else
180+
logger.debug(
181+
"visual_commands/neotree",
182+
"tree state win mismatch",
183+
"current_win=",
184+
current_win,
185+
"state.winid=",
186+
tostring(state.winid)
187+
)
167188
return nil, nil
168189
end
169190
elseif current_ft == "NvimTree" then
@@ -264,30 +285,61 @@ function M.get_files_from_visual_selection(visual_data)
264285
local files = {}
265286

266287
if tree_type == "neo-tree" then
288+
logger.debug("visual_commands/neotree", "processing visual selection", "range=", start_pos, "to", end_pos)
267289
local selected_nodes = {}
268290
for line = start_pos, end_pos do
269291
-- Neo-tree's tree:get_node() uses the line number directly (1-based)
270292
local node = tree_state.tree:get_node(line)
271293
if node then
272294
if node.type and node.type ~= "message" then
273295
table.insert(selected_nodes, node)
296+
local depth = (node.get_depth and node:get_depth()) or 0
297+
logger.debug(
298+
"visual_commands/neotree",
299+
"line",
300+
line,
301+
"node type=",
302+
tostring(node.type),
303+
"depth=",
304+
depth,
305+
"path=",
306+
tostring(node.path)
307+
)
308+
else
309+
logger.debug("visual_commands/neotree", "line", line, "node rejected (type)", tostring(node and node.type))
274310
end
311+
else
312+
logger.debug("visual_commands/neotree", "line", line, "no node returned from tree:get_node")
275313
end
276314
end
277315

316+
logger.debug("visual_commands/neotree", "selected_nodes count=", #selected_nodes)
317+
278318
for _, node in ipairs(selected_nodes) do
279319
if node.type == "file" and node.path and node.path ~= "" then
280320
local depth = (node.get_depth and node:get_depth()) or 0
281321
if depth > 1 then
282322
table.insert(files, node.path)
323+
else
324+
logger.debug("visual_commands/neotree", "rejected file (depth<=1)", node.path)
283325
end
284326
elseif node.type == "directory" and node.path and node.path ~= "" then
285327
local depth = (node.get_depth and node:get_depth()) or 0
286328
if depth > 1 then
287329
table.insert(files, node.path)
330+
else
331+
logger.debug("visual_commands/neotree", "rejected directory (depth<=1)", node.path)
288332
end
333+
else
334+
logger.debug(
335+
"visual_commands/neotree",
336+
"rejected node (missing path or unsupported type)",
337+
tostring(node and node.type),
338+
tostring(node and node.path)
339+
)
289340
end
290341
end
342+
logger.debug("visual_commands/neotree", "files from visual selection:", files)
291343
elseif tree_type == "nvim-tree" then
292344
-- For nvim-tree, we need to manually map visual lines to tree nodes
293345
-- since nvim-tree doesn't have direct line-to-node mapping like neo-tree

0 commit comments

Comments
 (0)