Skip to content

Commit b8e5e54

Browse files
jdurandThomasK33
andauthored
feat: add mini.files support (#89)
* fix: update coroutine context detection for Lua 5.4 compatibility Fix openDiff blocking operations failing in test environment due to incorrect coroutine context detection. In Lua 5.4, coroutine.running() returns (thread, is_main) where is_main indicates if running in main thread vs coroutine. Changes: - Update diff.lua: Fix open_diff_blocking coroutine check - Update tools/open_diff.lua: Fix handler coroutine validation - Update tests: Add proper mocking and expected error messages * 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 * 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. * feat: add test fixture for mini.files * refactor: replace duplicated dev config with symlink and remove unused variable Change-Id: I277a88d244cb84f517a4b899293a00408293041f Signed-off-by: Thomas Kosiewski <tk@coder.com> --------- Signed-off-by: Thomas Kosiewski <tk@coder.com> Co-authored-by: Thomas Kosiewski <tk@coder.com>
1 parent abcc0ff commit b8e5e54

File tree

14 files changed

+645
-6
lines changed

14 files changed

+645
-6
lines changed

ARCHITECTURE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ source fixtures/nvim-aliases.sh
246246
vv nvim-tree # Test with nvim-tree integration
247247
vv oil # Test with oil.nvim integration
248248
vv netrw # Test with built-in netrw
249+
vv mini-files # Test with built-in mini.files
249250

250251
# Each fixture provides:
251252
# - Complete Neovim configuration

DEVELOPMENT.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ claudecode.nvim/
99
├── .github/workflows/ # CI workflow definitions
1010
├── fixtures/ # Test Neovim configurations for integration testing
1111
│ ├── bin/ # Helper scripts (vv, vve, list-configs)
12+
│ ├── mini-files/ # Neovim config testing with mini.files
1213
│ ├── netrw/ # Neovim config testing with built-in file explorer
1314
│ ├── nvim-tree/ # Neovim config testing with nvim-tree.lua
1415
│ ├── oil/ # Neovim config testing with oil.nvim

fixtures/mini-files/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require("config.lazy")

fixtures/mini-files/lazy-lock.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lazy.nvim": { "branch": "main", "commit": "6c3bda4aca61a13a9c63f1c1d1b16b9d3be90d7a" },
3+
"mini.files": { "branch": "main", "commit": "5b9431cf5c69b8e69e5a67d2d12338a3ac2e1541" },
4+
"tokyonight.nvim": { "branch": "main", "commit": "057ef5d260c1931f1dffd0f052c685dcd14100a3" }
5+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- Bootstrap lazy.nvim
2+
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
3+
if not (vim.uv or vim.loop).fs_stat(lazypath) then
4+
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
5+
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
6+
if vim.v.shell_error ~= 0 then
7+
vim.api.nvim_echo({
8+
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
9+
{ out, "WarningMsg" },
10+
{ "\nPress any key to exit..." },
11+
}, true, {})
12+
vim.fn.getchar()
13+
os.exit(1)
14+
end
15+
end
16+
vim.opt.rtp:prepend(lazypath)
17+
18+
-- Make sure to setup `mapleader` and `maplocalleader` before
19+
-- loading lazy.nvim so that mappings are correct.
20+
-- This is also a good place to setup other settings (vim.opt)
21+
vim.g.mapleader = " "
22+
vim.g.maplocalleader = "\\"
23+
24+
-- Setup lazy.nvim
25+
require("lazy").setup({
26+
spec = {
27+
-- import your plugins
28+
{ import = "plugins" },
29+
},
30+
-- Configure any other settings here. See the documentation for more details.
31+
-- colorscheme that will be used when installing plugins.
32+
install = { colorscheme = { "habamax" } },
33+
-- automatically check for plugin updates
34+
checker = { enabled = true },
35+
})
36+
37+
-- Add keybind for Lazy plugin manager
38+
vim.keymap.set("n", "<leader>l", "<cmd>Lazy<cr>", { desc = "Lazy Plugin Manager" })
39+
40+
-- Terminal keybindings
41+
vim.keymap.set("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode (double esc)" })
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../dev-config.lua
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Basic plugin configuration
2+
return {
3+
-- Example: add a colorscheme
4+
{
5+
"folke/tokyonight.nvim",
6+
lazy = false,
7+
priority = 1000,
8+
config = function()
9+
vim.cmd([[colorscheme tokyonight]])
10+
end,
11+
},
12+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
return {
2+
"echasnovski/mini.files",
3+
version = false,
4+
config = function()
5+
require("mini.files").setup({
6+
-- Customization of shown content
7+
content = {
8+
-- Predicate for which file system entries to show
9+
filter = nil,
10+
-- What prefix to show to the left of file system entry
11+
prefix = nil,
12+
-- In which order to show file system entries
13+
sort = nil,
14+
},
15+
16+
-- Module mappings created only inside explorer.
17+
-- Use `''` (empty string) to not create one.
18+
mappings = {
19+
close = "q",
20+
go_in = "l",
21+
go_in_plus = "L",
22+
go_out = "h",
23+
go_out_plus = "H",
24+
reset = "<BS>",
25+
reveal_cwd = "@",
26+
show_help = "g?",
27+
synchronize = "=",
28+
trim_left = "<",
29+
trim_right = ">",
30+
},
31+
32+
-- General options
33+
options = {
34+
-- Whether to delete permanently or move into module-specific trash
35+
permanent_delete = true,
36+
-- Whether to use for editing directories
37+
use_as_default_explorer = true,
38+
},
39+
40+
-- Customization of explorer windows
41+
windows = {
42+
-- Maximum number of windows to show side by side
43+
max_number = math.huge,
44+
-- Whether to show preview of file/directory under cursor
45+
preview = false,
46+
-- Width of focused window
47+
width_focus = 50,
48+
-- Width of non-focused window
49+
width_nofocus = 15,
50+
-- Width of preview window
51+
width_preview = 25,
52+
},
53+
})
54+
55+
-- Global keybindings for mini.files
56+
vim.keymap.set("n", "<leader>e", function()
57+
require("mini.files").open()
58+
end, { desc = "Open mini.files (current dir)" })
59+
60+
vim.keymap.set("n", "<leader>E", function()
61+
require("mini.files").open(vim.api.nvim_buf_get_name(0))
62+
end, { desc = "Open mini.files (current file)" })
63+
64+
vim.keymap.set("n", "-", function()
65+
require("mini.files").open()
66+
end, { desc = "Open parent directory" })
67+
68+
-- Mini.files specific keybindings and autocommands
69+
vim.api.nvim_create_autocmd("User", {
70+
pattern = "MiniFilesBufferCreate",
71+
callback = function(args)
72+
local buf_id = args.data.buf_id
73+
74+
-- Add buffer-local keybindings
75+
vim.keymap.set("n", "<C-s>", function()
76+
-- Split window and open file
77+
local cur_target = require("mini.files").get_fs_entry()
78+
if cur_target and cur_target.fs_type == "file" then
79+
require("mini.files").close()
80+
vim.cmd("split " .. cur_target.path)
81+
end
82+
end, { buffer = buf_id, desc = "Split and open file" })
83+
84+
vim.keymap.set("n", "<C-v>", function()
85+
-- Vertical split and open file
86+
local cur_target = require("mini.files").get_fs_entry()
87+
if cur_target and cur_target.fs_type == "file" then
88+
require("mini.files").close()
89+
vim.cmd("vsplit " .. cur_target.path)
90+
end
91+
end, { buffer = buf_id, desc = "Vertical split and open file" })
92+
93+
vim.keymap.set("n", "<C-t>", function()
94+
-- Open in new tab
95+
local cur_target = require("mini.files").get_fs_entry()
96+
if cur_target and cur_target.fs_type == "file" then
97+
require("mini.files").close()
98+
vim.cmd("tabnew " .. cur_target.path)
99+
end
100+
end, { buffer = buf_id, desc = "Open in new tab" })
101+
102+
-- Create new file/directory
103+
vim.keymap.set("n", "a", function()
104+
local cur_target = require("mini.files").get_fs_entry()
105+
local path = cur_target and cur_target.path or require("mini.files").get_explorer_state().cwd
106+
local new_name = vim.fn.input("Create: " .. path .. "/")
107+
if new_name and new_name ~= "" then
108+
if new_name:sub(-1) == "/" then
109+
-- Create directory
110+
vim.fn.mkdir(path .. "/" .. new_name, "p")
111+
else
112+
-- Create file
113+
local new_file = io.open(path .. "/" .. new_name, "w")
114+
if new_file then
115+
new_file:close()
116+
end
117+
end
118+
require("mini.files").refresh()
119+
end
120+
end, { buffer = buf_id, desc = "Create new file/directory" })
121+
122+
-- Rename file/directory
123+
vim.keymap.set("n", "r", function()
124+
local cur_target = require("mini.files").get_fs_entry()
125+
if cur_target then
126+
local old_name = vim.fn.fnamemodify(cur_target.path, ":t")
127+
local new_name = vim.fn.input("Rename to: ", old_name)
128+
if new_name and new_name ~= "" and new_name ~= old_name then
129+
local new_path = vim.fn.fnamemodify(cur_target.path, ":h") .. "/" .. new_name
130+
os.rename(cur_target.path, new_path)
131+
require("mini.files").refresh()
132+
end
133+
end
134+
end, { buffer = buf_id, desc = "Rename file/directory" })
135+
136+
-- Delete file/directory
137+
vim.keymap.set("n", "d", function()
138+
local cur_target = require("mini.files").get_fs_entry()
139+
if cur_target then
140+
local confirm = vim.fn.confirm("Delete " .. cur_target.path .. "?", "&Yes\n&No", 2)
141+
if confirm == 1 then
142+
if cur_target.fs_type == "directory" then
143+
vim.fn.delete(cur_target.path, "rf")
144+
else
145+
vim.fn.delete(cur_target.path)
146+
end
147+
require("mini.files").refresh()
148+
end
149+
end
150+
end, { buffer = buf_id, desc = "Delete file/directory" })
151+
end,
152+
})
153+
154+
-- Auto-close mini.files when it's the last window
155+
vim.api.nvim_create_autocmd("User", {
156+
pattern = "MiniFilesBufferUpdate",
157+
callback = function()
158+
if vim.bo.filetype == "minifiles" then
159+
-- Check if this is the only window left
160+
local windows = vim.api.nvim_list_wins()
161+
local minifiles_windows = 0
162+
for _, win in ipairs(windows) do
163+
local buf = vim.api.nvim_win_get_buf(win)
164+
if vim.api.nvim_buf_get_option(buf, "filetype") == "minifiles" then
165+
minifiles_windows = minifiles_windows + 1
166+
end
167+
end
168+
169+
if #windows == minifiles_windows then
170+
vim.cmd("quit")
171+
end
172+
end
173+
end,
174+
})
175+
end,
176+
}

lua/claudecode/diff.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -770,8 +770,8 @@ function M.open_diff_blocking(old_file_path, new_file_path, new_file_contents, t
770770
end
771771

772772
-- Set up blocking diff operation
773-
local co = coroutine.running()
774-
if not co then
773+
local co, is_main = coroutine.running()
774+
if not co or is_main then
775775
error({
776776
code = -32000,
777777
message = "Internal server error",

lua/claudecode/init.lua

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,10 @@ function M._create_commands()
670670
local is_tree_buffer = current_ft == "NvimTree"
671671
or current_ft == "neo-tree"
672672
or current_ft == "oil"
673+
or current_ft == "minifiles"
673674
or string.match(current_bufname, "neo%-tree")
674675
or string.match(current_bufname, "NvimTree")
676+
or string.match(current_bufname, "minifiles://")
675677

676678
if is_tree_buffer then
677679
local integrations = require("claudecode.integrations")
@@ -715,7 +717,53 @@ function M._create_commands()
715717
end
716718

717719
local function handle_send_visual(visual_data, _opts)
718-
-- Try tree file selection first
720+
-- Check if we're in a tree buffer first
721+
local current_ft = (vim.bo and vim.bo.filetype) or ""
722+
local current_bufname = (vim.api and vim.api.nvim_buf_get_name and vim.api.nvim_buf_get_name(0)) or ""
723+
724+
local is_tree_buffer = current_ft == "NvimTree"
725+
or current_ft == "neo-tree"
726+
or current_ft == "oil"
727+
or current_ft == "minifiles"
728+
or string.match(current_bufname, "neo%-tree")
729+
or string.match(current_bufname, "NvimTree")
730+
or string.match(current_bufname, "minifiles://")
731+
732+
if is_tree_buffer then
733+
local integrations = require("claudecode.integrations")
734+
local files, error
735+
736+
-- For mini.files, try to get the range from visual marks
737+
if current_ft == "minifiles" or string.match(current_bufname, "minifiles://") then
738+
local start_line = vim.fn.line("'<")
739+
local end_line = vim.fn.line("'>")
740+
741+
if start_line > 0 and end_line > 0 and start_line <= end_line then
742+
-- Use range-based selection for mini.files
743+
files, error = integrations._get_mini_files_selection_with_range(start_line, end_line)
744+
else
745+
-- Fall back to regular method
746+
files, error = integrations.get_selected_files_from_tree()
747+
end
748+
else
749+
files, error = integrations.get_selected_files_from_tree()
750+
end
751+
752+
if error then
753+
logger.error("command", "ClaudeCodeSend_visual->TreeAdd: " .. error)
754+
return
755+
end
756+
757+
if not files or #files == 0 then
758+
logger.warn("command", "ClaudeCodeSend_visual->TreeAdd: No files selected")
759+
return
760+
end
761+
762+
add_paths_to_claude(files, { context = "ClaudeCodeSend_visual->TreeAdd" })
763+
return
764+
end
765+
766+
-- Fall back to old visual selection logic for non-tree buffers
719767
if visual_data then
720768
local visual_commands = require("claudecode.visual_commands")
721769
local files, error = visual_commands.get_files_from_visual_selection(visual_data)

0 commit comments

Comments
 (0)