Skip to content

Commit 07459d5

Browse files
committed
feat: add multi-file visual selection support to mini.files integration
This update introduces support for visual mode multi-file selection in mini.files integration. Users can now select multiple files in visual mode and send them all to Claude Code at once.
1 parent 24556db commit 07459d5

File tree

3 files changed

+156
-98
lines changed

3 files changed

+156
-98
lines changed

lua/claudecode/init.lua

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,10 @@ function M._create_commands()
611611
local is_tree_buffer = current_ft == "NvimTree"
612612
or current_ft == "neo-tree"
613613
or current_ft == "oil"
614+
or current_ft == "minifiles"
614615
or string.match(current_bufname, "neo%-tree")
615616
or string.match(current_bufname, "NvimTree")
617+
or string.match(current_bufname, "minifiles://")
616618

617619
if is_tree_buffer then
618620
local integrations = require("claudecode.integrations")
@@ -656,7 +658,54 @@ function M._create_commands()
656658
end
657659

658660
local function handle_send_visual(visual_data, _opts)
659-
-- Try tree file selection first
661+
-- Check if we're in a tree buffer first
662+
local current_ft = (vim.bo and vim.bo.filetype) or ""
663+
local current_bufname = (vim.api and vim.api.nvim_buf_get_name and vim.api.nvim_buf_get_name(0)) or ""
664+
665+
local is_tree_buffer = current_ft == "NvimTree"
666+
or current_ft == "neo-tree"
667+
or current_ft == "oil"
668+
or current_ft == "minifiles"
669+
or string.match(current_bufname, "neo%-tree")
670+
or string.match(current_bufname, "NvimTree")
671+
or string.match(current_bufname, "minifiles://")
672+
673+
if is_tree_buffer then
674+
local integrations = require("claudecode.integrations")
675+
local files, error
676+
677+
-- For mini.files, try to get the range from visual marks
678+
if current_ft == "minifiles" or string.match(current_bufname, "minifiles://") then
679+
local start_line = vim.fn.line("'<")
680+
local end_line = vim.fn.line("'>")
681+
682+
683+
if start_line > 0 and end_line > 0 and start_line <= end_line then
684+
-- Use range-based selection for mini.files
685+
files, error = integrations._get_mini_files_selection_with_range(start_line, end_line)
686+
else
687+
-- Fall back to regular method
688+
files, error = integrations.get_selected_files_from_tree()
689+
end
690+
else
691+
files, error = integrations.get_selected_files_from_tree()
692+
end
693+
694+
if error then
695+
logger.error("command", "ClaudeCodeSend_visual->TreeAdd: " .. error)
696+
return
697+
end
698+
699+
if not files or #files == 0 then
700+
logger.warn("command", "ClaudeCodeSend_visual->TreeAdd: No files selected")
701+
return
702+
end
703+
704+
add_paths_to_claude(files, { context = "ClaudeCodeSend_visual->TreeAdd" })
705+
return
706+
end
707+
708+
-- Fall back to old visual selection logic for non-tree buffers
660709
if visual_data then
661710
local visual_commands = require("claudecode.visual_commands")
662711
local files, error = visual_commands.get_files_from_visual_selection(visual_data)

lua/claudecode/integrations.lua

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -268,49 +268,73 @@ end
268268
--- Reference: mini.files API MiniFiles.get_fs_entry()
269269
--- @return table files List of file paths
270270
--- @return string|nil error Error message if operation failed
271-
function M._get_mini_files_selection()
271+
272+
-- Helper function to get mini.files selection using explicit range
273+
function M._get_mini_files_selection_with_range(start_line, end_line)
272274
local success, mini_files = pcall(require, "mini.files")
273275
if not success then
274276
return {}, "mini.files not available"
275277
end
276278

277279
local files = {}
278-
279-
-- Check if we're in visual mode for multi-selection
280-
local mode = vim.fn.mode()
281-
if mode == "V" or mode == "v" or mode == "\22" then
282-
-- Visual mode: get visual range
283-
local visual_commands = require("claudecode.visual_commands")
284-
local start_line, end_line = visual_commands.get_visual_range()
285-
286-
-- Process each line in the visual selection
287-
for line = start_line, end_line do
288-
local entry_ok, entry = pcall(mini_files.get_fs_entry, nil, line)
289-
if entry_ok and entry and entry.path then
290-
-- Validate that the path exists
291-
if vim.fn.filereadable(entry.path) == 1 or vim.fn.isdirectory(entry.path) == 1 then
292-
table.insert(files, entry.path)
293-
end
280+
local bufnr = vim.api.nvim_get_current_buf()
281+
282+
-- Process each line in the range
283+
for line = start_line, end_line do
284+
local entry_ok, entry = pcall(mini_files.get_fs_entry, bufnr, line)
285+
286+
if entry_ok and entry and entry.path and entry.path ~= "" then
287+
-- Extract real filesystem path from mini.files buffer path
288+
local real_path = entry.path
289+
-- Remove mini.files buffer protocol prefix if present
290+
if real_path:match("^minifiles://") then
291+
real_path = real_path:gsub("^minifiles://[^/]*/", "")
294292
end
295-
end
296293

297-
if #files > 0 then
298-
return files, nil
294+
-- Validate that the path exists
295+
if vim.fn.filereadable(real_path) == 1 or vim.fn.isdirectory(real_path) == 1 then
296+
table.insert(files, real_path)
297+
end
299298
end
299+
end
300+
301+
if #files > 0 then
302+
return files, nil
300303
else
301-
-- Normal mode: get file under cursor
302-
local entry_ok, entry = pcall(mini_files.get_fs_entry)
303-
if not entry_ok or not entry then
304-
return {}, "Failed to get entry from mini.files"
305-
end
304+
return {}, "No files found in range"
305+
end
306+
end
306307

307-
if entry.path and entry.path ~= "" then
308-
-- Validate that the path exists
309-
if vim.fn.filereadable(entry.path) == 1 or vim.fn.isdirectory(entry.path) == 1 then
310-
return { entry.path }, nil
311-
else
312-
return {}, "Invalid file or directory path: " .. entry.path
313-
end
308+
function M._get_mini_files_selection()
309+
local success, mini_files = pcall(require, "mini.files")
310+
if not success then
311+
return {}, "mini.files not available"
312+
end
313+
314+
local files = {}
315+
316+
local bufnr = vim.api.nvim_get_current_buf()
317+
318+
319+
-- Normal mode: get file under cursor
320+
local entry_ok, entry = pcall(mini_files.get_fs_entry, bufnr)
321+
if not entry_ok or not entry then
322+
return {}, "Failed to get entry from mini.files"
323+
end
324+
325+
if entry.path and entry.path ~= "" then
326+
-- Extract real filesystem path from mini.files buffer path
327+
local real_path = entry.path
328+
-- Remove mini.files buffer protocol prefix if present
329+
if real_path:match("^minifiles://") then
330+
real_path = real_path:gsub("^minifiles://[^/]*/", "")
331+
end
332+
333+
-- Validate that the path exists
334+
if vim.fn.filereadable(real_path) == 1 or vim.fn.isdirectory(real_path) == 1 then
335+
return { real_path }, nil
336+
else
337+
return {}, "Invalid file or directory path: " .. real_path
314338
end
315339
end
316340

tests/unit/mini_files_integration_spec.lua

Lines changed: 50 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ describe("mini.files integration", function()
4242
return 0
4343
end,
4444
},
45+
api = {
46+
nvim_get_current_buf = function()
47+
return 1 -- Mock buffer ID
48+
end
49+
},
4550
bo = { filetype = "minifiles" },
4651
}
4752

@@ -57,7 +62,11 @@ describe("mini.files integration", function()
5762
it("should get single file under cursor", function()
5863
-- Mock mini.files module
5964
local mock_mini_files = {
60-
get_fs_entry = function()
65+
get_fs_entry = function(buf_id)
66+
-- Verify buffer ID is passed correctly
67+
if buf_id ~= 1 then
68+
return nil
69+
end
6170
return { path = "/Users/test/project/main.lua" }
6271
end,
6372
}
@@ -74,7 +83,11 @@ describe("mini.files integration", function()
7483
it("should get directory under cursor", function()
7584
-- Mock mini.files module
7685
local mock_mini_files = {
77-
get_fs_entry = function()
86+
get_fs_entry = function(buf_id)
87+
-- Verify buffer ID is passed correctly
88+
if buf_id ~= 1 then
89+
return nil
90+
end
7891
return { path = "/Users/test/project/src" }
7992
end,
8093
}
@@ -88,22 +101,14 @@ describe("mini.files integration", function()
88101
expect(files[1]).to_be("/Users/test/project/src")
89102
end)
90103

91-
it("should get multiple files in visual mode", function()
92-
mock_vim.fn.mode = function()
93-
return "V" -- Visual line mode
94-
end
95-
96-
-- Mock mini.files module
104+
it("should handle mini.files buffer path format", function()
105+
-- Mock mini.files module that returns buffer-style paths
97106
local mock_mini_files = {
98-
get_fs_entry = function(buf_id, line)
99-
if line == 1 then
100-
return { path = "/Users/test/project/file1.lua" }
101-
elseif line == 2 then
102-
return { path = "/Users/test/project/file2.lua" }
103-
elseif line == 3 then
104-
return { path = "/Users/test/project/src" }
107+
get_fs_entry = function(buf_id)
108+
if buf_id ~= 1 then
109+
return nil
105110
end
106-
return nil
111+
return { path = "minifiles://42//Users/test/project/buffer_file.lua" }
107112
end,
108113
}
109114
package.loaded["mini.files"] = mock_mini_files
@@ -112,41 +117,40 @@ describe("mini.files integration", function()
112117

113118
expect(err).to_be_nil()
114119
expect(files).to_be_table()
115-
expect(#files).to_be(3)
116-
expect(files[1]).to_be("/Users/test/project/file1.lua")
117-
expect(files[2]).to_be("/Users/test/project/file2.lua")
118-
expect(files[3]).to_be("/Users/test/project/src")
120+
expect(#files).to_be(1)
121+
expect(files[1]).to_be("/Users/test/project/buffer_file.lua")
119122
end)
120123

121-
it("should filter out invalid files in visual mode", function()
122-
mock_vim.fn.mode = function()
123-
return "V" -- Visual line mode
124-
end
125-
126-
-- Mock mini.files module
127-
local mock_mini_files = {
128-
get_fs_entry = function(buf_id, line)
129-
if line == 1 then
130-
return { path = "/Users/test/project/valid.lua" }
131-
elseif line == 2 then
132-
return { path = "/Users/test/project/invalid.xyz" } -- Won't pass filereadable/isdirectory
133-
elseif line == 3 then
134-
return { path = "/Users/test/project/src" }
135-
end
136-
return nil
137-
end,
124+
it("should handle various mini.files buffer path formats", function()
125+
-- Test different buffer path formats that could occur
126+
local test_cases = {
127+
{ input = "minifiles://42/Users/test/file.lua", expected = "Users/test/file.lua" },
128+
{ input = "minifiles://42//Users/test/file.lua", expected = "/Users/test/file.lua" },
129+
{ input = "minifiles://123///Users/test/file.lua", expected = "//Users/test/file.lua" },
130+
{ input = "/Users/test/normal_path.lua", expected = "/Users/test/normal_path.lua" },
138131
}
139-
package.loaded["mini.files"] = mock_mini_files
140-
141-
local files, err = integrations._get_mini_files_selection()
142132

143-
expect(err).to_be_nil()
144-
expect(files).to_be_table()
145-
expect(#files).to_be(2) -- Only valid.lua and src
146-
expect(files[1]).to_be("/Users/test/project/valid.lua")
147-
expect(files[2]).to_be("/Users/test/project/src")
133+
for i, test_case in ipairs(test_cases) do
134+
local mock_mini_files = {
135+
get_fs_entry = function(buf_id)
136+
if buf_id ~= 1 then
137+
return nil
138+
end
139+
return { path = test_case.input }
140+
end,
141+
}
142+
package.loaded["mini.files"] = mock_mini_files
143+
144+
local files, err = integrations._get_mini_files_selection()
145+
146+
expect(err).to_be_nil()
147+
expect(files).to_be_table()
148+
expect(#files).to_be(1)
149+
expect(files[1]).to_be(test_case.expected)
150+
end
148151
end)
149152

153+
150154
it("should handle empty entry under cursor", function()
151155
-- Mock mini.files module
152156
local mock_mini_files = {
@@ -229,25 +233,6 @@ describe("mini.files integration", function()
229233
expect(#files).to_be(0)
230234
end)
231235

232-
it("should handle visual mode with no valid entries", function()
233-
mock_vim.fn.mode = function()
234-
return "V" -- Visual line mode
235-
end
236-
237-
-- Mock mini.files module
238-
local mock_mini_files = {
239-
get_fs_entry = function(buf_id, line)
240-
return nil -- No entries
241-
end,
242-
}
243-
package.loaded["mini.files"] = mock_mini_files
244-
245-
local files, err = integrations._get_mini_files_selection()
246-
247-
expect(err).to_be("No file found under cursor")
248-
expect(files).to_be_table()
249-
expect(#files).to_be(0)
250-
end)
251236
end)
252237

253238
describe("get_selected_files_from_tree", function()
@@ -279,4 +264,4 @@ describe("mini.files integration", function()
279264
expect(files).to_be_nil()
280265
end)
281266
end)
282-
end)
267+
end)

0 commit comments

Comments
 (0)