Skip to content

Commit 58c17e4

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 556af30 commit 58c17e4

File tree

3 files changed

+151
-97
lines changed

3 files changed

+151
-97
lines changed

lua/claudecode/init.lua

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,8 +614,10 @@ function M._create_commands()
614614
local is_tree_buffer = current_ft == "NvimTree"
615615
or current_ft == "neo-tree"
616616
or current_ft == "oil"
617+
or current_ft == "minifiles"
617618
or string.match(current_bufname, "neo%-tree")
618619
or string.match(current_bufname, "NvimTree")
620+
or string.match(current_bufname, "minifiles://")
619621

620622
if is_tree_buffer then
621623
local integrations = require("claudecode.integrations")
@@ -659,7 +661,53 @@ function M._create_commands()
659661
end
660662

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

lua/claudecode/integrations.lua

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -268,49 +268,72 @@ 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 = {}
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://[^/]*/", "")
292+
end
278293

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
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)
294297
end
295298
end
299+
end
296300

297-
if #files > 0 then
298-
return files, nil
299-
end
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"
304+
return {}, "No files found in range"
305+
end
306+
end
307+
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+
-- Normal mode: get file under cursor
319+
local entry_ok, entry = pcall(mini_files.get_fs_entry, bufnr)
320+
if not entry_ok or not entry then
321+
return {}, "Failed to get entry from mini.files"
322+
end
323+
324+
if entry.path and entry.path ~= "" then
325+
-- Extract real filesystem path from mini.files buffer path
326+
local real_path = entry.path
327+
-- Remove mini.files buffer protocol prefix if present
328+
if real_path:match("^minifiles://") then
329+
real_path = real_path:gsub("^minifiles://[^/]*/", "")
305330
end
306331

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
332+
-- Validate that the path exists
333+
if vim.fn.filereadable(real_path) == 1 or vim.fn.isdirectory(real_path) == 1 then
334+
return { real_path }, nil
335+
else
336+
return {}, "Invalid file or directory path: " .. real_path
314337
end
315338
end
316339

tests/unit/mini_files_integration_spec.lua

Lines changed: 49 additions & 66 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,39 +117,37 @@ 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

150153
it("should handle empty entry under cursor", function()
@@ -228,26 +231,6 @@ describe("mini.files integration", function()
228231
expect(files).to_be_table()
229232
expect(#files).to_be(0)
230233
end)
231-
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)
251234
end)
252235

253236
describe("get_selected_files_from_tree", function()
@@ -279,4 +262,4 @@ describe("mini.files integration", function()
279262
expect(files).to_be_nil()
280263
end)
281264
end)
282-
end)
265+
end)

0 commit comments

Comments
 (0)