Skip to content

Commit 556af30

Browse files
committed
feat: add mini.files support to claudecode.nvim
Add mini.files integration following the same pattern as netrw support. - Add _get_mini_files_selection() function to integrations.lua - Support both visual selection and single file under cursor - Add comprehensive test suite with 12 test cases - Handle error cases and edge conditions gracefully
1 parent 13e1fc3 commit 556af30

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed

lua/claudecode/integrations.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ function M.get_selected_files_from_tree()
1616
return M._get_neotree_selection()
1717
elseif current_ft == "oil" then
1818
return M._get_oil_selection()
19+
elseif current_ft == "minifiles" then
20+
return M._get_mini_files_selection()
1921
else
2022
return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")"
2123
end
@@ -261,4 +263,58 @@ function M._get_oil_selection()
261263
return {}, "No file found under cursor"
262264
end
263265

266+
--- Get selected files from mini.files
267+
--- Supports both visual selection and single file under cursor
268+
--- Reference: mini.files API MiniFiles.get_fs_entry()
269+
--- @return table files List of file paths
270+
--- @return string|nil error Error message if operation failed
271+
function M._get_mini_files_selection()
272+
local success, mini_files = pcall(require, "mini.files")
273+
if not success then
274+
return {}, "mini.files not available"
275+
end
276+
277+
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
294+
end
295+
end
296+
297+
if #files > 0 then
298+
return files, nil
299+
end
300+
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
306+
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
314+
end
315+
end
316+
317+
return {}, "No file found under cursor"
318+
end
319+
264320
return M
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
-- luacheck: globals expect
2+
require("tests.busted_setup")
3+
4+
describe("mini.files integration", function()
5+
local integrations
6+
local mock_vim
7+
8+
local function setup_mocks()
9+
package.loaded["claudecode.integrations"] = nil
10+
package.loaded["claudecode.logger"] = nil
11+
package.loaded["claudecode.visual_commands"] = nil
12+
13+
-- Mock logger
14+
package.loaded["claudecode.logger"] = {
15+
debug = function() end,
16+
warn = function() end,
17+
error = function() end,
18+
}
19+
20+
-- Mock visual_commands
21+
package.loaded["claudecode.visual_commands"] = {
22+
get_visual_range = function()
23+
return 1, 3 -- Return lines 1-3 by default
24+
end,
25+
}
26+
27+
mock_vim = {
28+
fn = {
29+
mode = function()
30+
return "n" -- Normal mode by default
31+
end,
32+
filereadable = function(path)
33+
if path:match("%.lua$") or path:match("%.txt$") then
34+
return 1
35+
end
36+
return 0
37+
end,
38+
isdirectory = function(path)
39+
if path:match("/$") or path:match("/src$") then
40+
return 1
41+
end
42+
return 0
43+
end,
44+
},
45+
bo = { filetype = "minifiles" },
46+
}
47+
48+
_G.vim = mock_vim
49+
end
50+
51+
before_each(function()
52+
setup_mocks()
53+
integrations = require("claudecode.integrations")
54+
end)
55+
56+
describe("_get_mini_files_selection", function()
57+
it("should get single file under cursor", function()
58+
-- Mock mini.files module
59+
local mock_mini_files = {
60+
get_fs_entry = function()
61+
return { path = "/Users/test/project/main.lua" }
62+
end,
63+
}
64+
package.loaded["mini.files"] = mock_mini_files
65+
66+
local files, err = integrations._get_mini_files_selection()
67+
68+
expect(err).to_be_nil()
69+
expect(files).to_be_table()
70+
expect(#files).to_be(1)
71+
expect(files[1]).to_be("/Users/test/project/main.lua")
72+
end)
73+
74+
it("should get directory under cursor", function()
75+
-- Mock mini.files module
76+
local mock_mini_files = {
77+
get_fs_entry = function()
78+
return { path = "/Users/test/project/src" }
79+
end,
80+
}
81+
package.loaded["mini.files"] = mock_mini_files
82+
83+
local files, err = integrations._get_mini_files_selection()
84+
85+
expect(err).to_be_nil()
86+
expect(files).to_be_table()
87+
expect(#files).to_be(1)
88+
expect(files[1]).to_be("/Users/test/project/src")
89+
end)
90+
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
97+
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" }
105+
end
106+
return nil
107+
end,
108+
}
109+
package.loaded["mini.files"] = mock_mini_files
110+
111+
local files, err = integrations._get_mini_files_selection()
112+
113+
expect(err).to_be_nil()
114+
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")
119+
end)
120+
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,
138+
}
139+
package.loaded["mini.files"] = mock_mini_files
140+
141+
local files, err = integrations._get_mini_files_selection()
142+
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")
148+
end)
149+
150+
it("should handle empty entry under cursor", function()
151+
-- Mock mini.files module
152+
local mock_mini_files = {
153+
get_fs_entry = function()
154+
return nil -- No entry
155+
end,
156+
}
157+
package.loaded["mini.files"] = mock_mini_files
158+
159+
local files, err = integrations._get_mini_files_selection()
160+
161+
expect(err).to_be("Failed to get entry from mini.files")
162+
expect(files).to_be_table()
163+
expect(#files).to_be(0)
164+
end)
165+
166+
it("should handle entry with empty path", function()
167+
-- Mock mini.files module
168+
local mock_mini_files = {
169+
get_fs_entry = function()
170+
return { path = "" } -- Empty path
171+
end,
172+
}
173+
package.loaded["mini.files"] = mock_mini_files
174+
175+
local files, err = integrations._get_mini_files_selection()
176+
177+
expect(err).to_be("No file found under cursor")
178+
expect(files).to_be_table()
179+
expect(#files).to_be(0)
180+
end)
181+
182+
it("should handle invalid file path", function()
183+
-- Mock mini.files module
184+
local mock_mini_files = {
185+
get_fs_entry = function()
186+
return { path = "/Users/test/project/invalid_file" }
187+
end,
188+
}
189+
package.loaded["mini.files"] = mock_mini_files
190+
191+
mock_vim.fn.filereadable = function()
192+
return 0 -- File not readable
193+
end
194+
mock_vim.fn.isdirectory = function()
195+
return 0 -- Not a directory
196+
end
197+
198+
local files, err = integrations._get_mini_files_selection()
199+
200+
expect(err).to_be("Invalid file or directory path: /Users/test/project/invalid_file")
201+
expect(files).to_be_table()
202+
expect(#files).to_be(0)
203+
end)
204+
205+
it("should handle mini.files not available", function()
206+
-- Don't mock mini.files module (will cause require to fail)
207+
package.loaded["mini.files"] = nil
208+
209+
local files, err = integrations._get_mini_files_selection()
210+
211+
expect(err).to_be("mini.files not available")
212+
expect(files).to_be_table()
213+
expect(#files).to_be(0)
214+
end)
215+
216+
it("should handle pcall errors gracefully", function()
217+
-- Mock mini.files module that throws errors
218+
local mock_mini_files = {
219+
get_fs_entry = function()
220+
error("mini.files get_fs_entry failed")
221+
end,
222+
}
223+
package.loaded["mini.files"] = mock_mini_files
224+
225+
local files, err = integrations._get_mini_files_selection()
226+
227+
expect(err).to_be("Failed to get entry from mini.files")
228+
expect(files).to_be_table()
229+
expect(#files).to_be(0)
230+
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)
251+
end)
252+
253+
describe("get_selected_files_from_tree", function()
254+
it("should detect minifiles filetype and delegate to _get_mini_files_selection", function()
255+
mock_vim.bo.filetype = "minifiles"
256+
257+
-- Mock mini.files module
258+
local mock_mini_files = {
259+
get_fs_entry = function()
260+
return { path = "/path/test.lua" }
261+
end,
262+
}
263+
package.loaded["mini.files"] = mock_mini_files
264+
265+
local files, err = integrations.get_selected_files_from_tree()
266+
267+
expect(err).to_be_nil()
268+
expect(files).to_be_table()
269+
expect(#files).to_be(1)
270+
expect(files[1]).to_be("/path/test.lua")
271+
end)
272+
273+
it("should return error for unsupported filetype", function()
274+
mock_vim.bo.filetype = "unsupported"
275+
276+
local files, err = integrations.get_selected_files_from_tree()
277+
278+
assert_contains(err, "Not in a supported tree buffer")
279+
expect(files).to_be_nil()
280+
end)
281+
end)
282+
end)

0 commit comments

Comments
 (0)