From 13e1fc338166e1a1c3e5005a10daaba15809a761 Mon Sep 17 00:00:00 2001 From: Jim Durand Date: Tue, 29 Jul 2025 02:11:57 -0400 Subject: [PATCH 1/5] 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 --- lua/claudecode/diff.lua | 4 ++-- lua/claudecode/tools/open_diff.lua | 4 ++-- tests/unit/opendiff_blocking_spec.lua | 13 ++++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lua/claudecode/diff.lua b/lua/claudecode/diff.lua index 852e8d8..631ac36 100644 --- a/lua/claudecode/diff.lua +++ b/lua/claudecode/diff.lua @@ -770,8 +770,8 @@ function M.open_diff_blocking(old_file_path, new_file_path, new_file_contents, t end -- Set up blocking diff operation - local co = coroutine.running() - if not co then + local co, is_main = coroutine.running() + if not co or is_main then error({ code = -32000, message = "Internal server error", diff --git a/lua/claudecode/tools/open_diff.lua b/lua/claudecode/tools/open_diff.lua index 18525a9..86145d5 100644 --- a/lua/claudecode/tools/open_diff.lua +++ b/lua/claudecode/tools/open_diff.lua @@ -52,8 +52,8 @@ local function handler(params) end -- Ensure we're running in a coroutine context for blocking operation - local co = coroutine.running() - if not co then + local co, is_main = coroutine.running() + if not co or is_main then error({ code = -32000, message = "Internal server error", diff --git a/tests/unit/opendiff_blocking_spec.lua b/tests/unit/opendiff_blocking_spec.lua index 854c639..6c103fe 100644 --- a/tests/unit/opendiff_blocking_spec.lua +++ b/tests/unit/opendiff_blocking_spec.lua @@ -18,6 +18,13 @@ describe("openDiff blocking behavior", function() package.loaded["claudecode.logger"] = mock_logger + -- Mock diff module to prevent loading issues + package.loaded["claudecode.diff"] = { + open_diff_blocking = function() + error("This should not be called in coroutine context test") + end, + } + -- Load the module under test open_diff_module = require("claudecode.tools.open_diff") end) @@ -121,7 +128,11 @@ describe("openDiff blocking behavior", function() assert.is_table(err) assert.equals(-32000, err.code) -- Error gets wrapped by open_diff_blocking -- The exact error message may vary depending on where it fails in the test environment - assert.is_true(err.message == "Error setting up diff" or err.message == "Internal server error") + assert.is_true( + err.message == "Error setting up diff" + or err.message == "Internal server error" + or err.message == "Error opening blocking diff" + ) end) it("should validate required parameters", function() From 556af30e9b160da14eab76ba835ccb1253d0dc9e Mon Sep 17 00:00:00 2001 From: Jim Durand Date: Mon, 28 Jul 2025 23:47:33 -0400 Subject: [PATCH 2/5] 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 --- lua/claudecode/integrations.lua | 56 ++++ tests/unit/mini_files_integration_spec.lua | 282 +++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 tests/unit/mini_files_integration_spec.lua diff --git a/lua/claudecode/integrations.lua b/lua/claudecode/integrations.lua index 2827aab..4937b3b 100644 --- a/lua/claudecode/integrations.lua +++ b/lua/claudecode/integrations.lua @@ -16,6 +16,8 @@ function M.get_selected_files_from_tree() return M._get_neotree_selection() elseif current_ft == "oil" then return M._get_oil_selection() + elseif current_ft == "minifiles" then + return M._get_mini_files_selection() else return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")" end @@ -261,4 +263,58 @@ function M._get_oil_selection() return {}, "No file found under cursor" end +--- Get selected files from mini.files +--- Supports both visual selection and single file under cursor +--- Reference: mini.files API MiniFiles.get_fs_entry() +--- @return table files List of file paths +--- @return string|nil error Error message if operation failed +function M._get_mini_files_selection() + local success, mini_files = pcall(require, "mini.files") + if not success then + return {}, "mini.files not available" + end + + local files = {} + + -- Check if we're in visual mode for multi-selection + local mode = vim.fn.mode() + if mode == "V" or mode == "v" or mode == "\22" then + -- Visual mode: get visual range + local visual_commands = require("claudecode.visual_commands") + local start_line, end_line = visual_commands.get_visual_range() + + -- Process each line in the visual selection + for line = start_line, end_line do + local entry_ok, entry = pcall(mini_files.get_fs_entry, nil, line) + if entry_ok and entry and entry.path then + -- Validate that the path exists + if vim.fn.filereadable(entry.path) == 1 or vim.fn.isdirectory(entry.path) == 1 then + table.insert(files, entry.path) + end + end + end + + if #files > 0 then + return files, nil + end + else + -- Normal mode: get file under cursor + local entry_ok, entry = pcall(mini_files.get_fs_entry) + if not entry_ok or not entry then + return {}, "Failed to get entry from mini.files" + end + + if entry.path and entry.path ~= "" then + -- Validate that the path exists + if vim.fn.filereadable(entry.path) == 1 or vim.fn.isdirectory(entry.path) == 1 then + return { entry.path }, nil + else + return {}, "Invalid file or directory path: " .. entry.path + end + end + end + + return {}, "No file found under cursor" +end + return M diff --git a/tests/unit/mini_files_integration_spec.lua b/tests/unit/mini_files_integration_spec.lua new file mode 100644 index 0000000..bd7cb01 --- /dev/null +++ b/tests/unit/mini_files_integration_spec.lua @@ -0,0 +1,282 @@ +-- luacheck: globals expect +require("tests.busted_setup") + +describe("mini.files integration", function() + local integrations + local mock_vim + + local function setup_mocks() + package.loaded["claudecode.integrations"] = nil + package.loaded["claudecode.logger"] = nil + package.loaded["claudecode.visual_commands"] = nil + + -- Mock logger + package.loaded["claudecode.logger"] = { + debug = function() end, + warn = function() end, + error = function() end, + } + + -- Mock visual_commands + package.loaded["claudecode.visual_commands"] = { + get_visual_range = function() + return 1, 3 -- Return lines 1-3 by default + end, + } + + mock_vim = { + fn = { + mode = function() + return "n" -- Normal mode by default + end, + filereadable = function(path) + if path:match("%.lua$") or path:match("%.txt$") then + return 1 + end + return 0 + end, + isdirectory = function(path) + if path:match("/$") or path:match("/src$") then + return 1 + end + return 0 + end, + }, + bo = { filetype = "minifiles" }, + } + + _G.vim = mock_vim + end + + before_each(function() + setup_mocks() + integrations = require("claudecode.integrations") + end) + + describe("_get_mini_files_selection", function() + it("should get single file under cursor", function() + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function() + return { path = "/Users/test/project/main.lua" } + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(1) + expect(files[1]).to_be("/Users/test/project/main.lua") + end) + + it("should get directory under cursor", function() + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function() + return { path = "/Users/test/project/src" } + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(1) + expect(files[1]).to_be("/Users/test/project/src") + end) + + it("should get multiple files in visual mode", function() + mock_vim.fn.mode = function() + return "V" -- Visual line mode + end + + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function(buf_id, line) + if line == 1 then + return { path = "/Users/test/project/file1.lua" } + elseif line == 2 then + return { path = "/Users/test/project/file2.lua" } + elseif line == 3 then + return { path = "/Users/test/project/src" } + end + return nil + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(3) + expect(files[1]).to_be("/Users/test/project/file1.lua") + expect(files[2]).to_be("/Users/test/project/file2.lua") + expect(files[3]).to_be("/Users/test/project/src") + end) + + it("should filter out invalid files in visual mode", function() + mock_vim.fn.mode = function() + return "V" -- Visual line mode + end + + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function(buf_id, line) + if line == 1 then + return { path = "/Users/test/project/valid.lua" } + elseif line == 2 then + return { path = "/Users/test/project/invalid.xyz" } -- Won't pass filereadable/isdirectory + elseif line == 3 then + return { path = "/Users/test/project/src" } + end + return nil + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(2) -- Only valid.lua and src + expect(files[1]).to_be("/Users/test/project/valid.lua") + expect(files[2]).to_be("/Users/test/project/src") + end) + + it("should handle empty entry under cursor", function() + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function() + return nil -- No entry + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be("Failed to get entry from mini.files") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle entry with empty path", function() + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function() + return { path = "" } -- Empty path + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be("No file found under cursor") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle invalid file path", function() + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function() + return { path = "/Users/test/project/invalid_file" } + end, + } + package.loaded["mini.files"] = mock_mini_files + + mock_vim.fn.filereadable = function() + return 0 -- File not readable + end + mock_vim.fn.isdirectory = function() + return 0 -- Not a directory + end + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be("Invalid file or directory path: /Users/test/project/invalid_file") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle mini.files not available", function() + -- Don't mock mini.files module (will cause require to fail) + package.loaded["mini.files"] = nil + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be("mini.files not available") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle pcall errors gracefully", function() + -- Mock mini.files module that throws errors + local mock_mini_files = { + get_fs_entry = function() + error("mini.files get_fs_entry failed") + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be("Failed to get entry from mini.files") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle visual mode with no valid entries", function() + mock_vim.fn.mode = function() + return "V" -- Visual line mode + end + + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function(buf_id, line) + return nil -- No entries + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be("No file found under cursor") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + end) + + describe("get_selected_files_from_tree", function() + it("should detect minifiles filetype and delegate to _get_mini_files_selection", function() + mock_vim.bo.filetype = "minifiles" + + -- Mock mini.files module + local mock_mini_files = { + get_fs_entry = function() + return { path = "/path/test.lua" } + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations.get_selected_files_from_tree() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(1) + expect(files[1]).to_be("/path/test.lua") + end) + + it("should return error for unsupported filetype", function() + mock_vim.bo.filetype = "unsupported" + + local files, err = integrations.get_selected_files_from_tree() + + assert_contains(err, "Not in a supported tree buffer") + expect(files).to_be_nil() + end) + end) +end) \ No newline at end of file From 58c17e4952dcb5b84e4b4b0604bf8255a951b4b9 Mon Sep 17 00:00:00 2001 From: Jim Durand Date: Tue, 29 Jul 2025 00:06:04 -0400 Subject: [PATCH 3/5] 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. --- lua/claudecode/init.lua | 50 ++++++++- lua/claudecode/integrations.lua | 83 +++++++++------ tests/unit/mini_files_integration_spec.lua | 115 +++++++++------------ 3 files changed, 151 insertions(+), 97 deletions(-) diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index dcaf16f..1cacd83 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -614,8 +614,10 @@ function M._create_commands() local is_tree_buffer = current_ft == "NvimTree" or current_ft == "neo-tree" or current_ft == "oil" + or current_ft == "minifiles" or string.match(current_bufname, "neo%-tree") or string.match(current_bufname, "NvimTree") + or string.match(current_bufname, "minifiles://") if is_tree_buffer then local integrations = require("claudecode.integrations") @@ -659,7 +661,53 @@ function M._create_commands() end local function handle_send_visual(visual_data, _opts) - -- Try tree file selection first + -- Check if we're in a tree buffer first + local current_ft = (vim.bo and vim.bo.filetype) or "" + local current_bufname = (vim.api and vim.api.nvim_buf_get_name and vim.api.nvim_buf_get_name(0)) or "" + + local is_tree_buffer = current_ft == "NvimTree" + or current_ft == "neo-tree" + or current_ft == "oil" + or current_ft == "minifiles" + or string.match(current_bufname, "neo%-tree") + or string.match(current_bufname, "NvimTree") + or string.match(current_bufname, "minifiles://") + + if is_tree_buffer then + local integrations = require("claudecode.integrations") + local files, error + + -- For mini.files, try to get the range from visual marks + if current_ft == "minifiles" or string.match(current_bufname, "minifiles://") then + local start_line = vim.fn.line("'<") + local end_line = vim.fn.line("'>") + + if start_line > 0 and end_line > 0 and start_line <= end_line then + -- Use range-based selection for mini.files + files, error = integrations._get_mini_files_selection_with_range(start_line, end_line) + else + -- Fall back to regular method + files, error = integrations.get_selected_files_from_tree() + end + else + files, error = integrations.get_selected_files_from_tree() + end + + if error then + logger.error("command", "ClaudeCodeSend_visual->TreeAdd: " .. error) + return + end + + if not files or #files == 0 then + logger.warn("command", "ClaudeCodeSend_visual->TreeAdd: No files selected") + return + end + + add_paths_to_claude(files, { context = "ClaudeCodeSend_visual->TreeAdd" }) + return + end + + -- Fall back to old visual selection logic for non-tree buffers if visual_data then local visual_commands = require("claudecode.visual_commands") local files, error = visual_commands.get_files_from_visual_selection(visual_data) diff --git a/lua/claudecode/integrations.lua b/lua/claudecode/integrations.lua index 4937b3b..759c5d6 100644 --- a/lua/claudecode/integrations.lua +++ b/lua/claudecode/integrations.lua @@ -268,49 +268,72 @@ end --- Reference: mini.files API MiniFiles.get_fs_entry() --- @return table files List of file paths --- @return string|nil error Error message if operation failed -function M._get_mini_files_selection() + +-- Helper function to get mini.files selection using explicit range +function M._get_mini_files_selection_with_range(start_line, end_line) local success, mini_files = pcall(require, "mini.files") if not success then return {}, "mini.files not available" end local files = {} + local bufnr = vim.api.nvim_get_current_buf() + + -- Process each line in the range + for line = start_line, end_line do + local entry_ok, entry = pcall(mini_files.get_fs_entry, bufnr, line) + + if entry_ok and entry and entry.path and entry.path ~= "" then + -- Extract real filesystem path from mini.files buffer path + local real_path = entry.path + -- Remove mini.files buffer protocol prefix if present + if real_path:match("^minifiles://") then + real_path = real_path:gsub("^minifiles://[^/]*/", "") + end - -- Check if we're in visual mode for multi-selection - local mode = vim.fn.mode() - if mode == "V" or mode == "v" or mode == "\22" then - -- Visual mode: get visual range - local visual_commands = require("claudecode.visual_commands") - local start_line, end_line = visual_commands.get_visual_range() - - -- Process each line in the visual selection - for line = start_line, end_line do - local entry_ok, entry = pcall(mini_files.get_fs_entry, nil, line) - if entry_ok and entry and entry.path then - -- Validate that the path exists - if vim.fn.filereadable(entry.path) == 1 or vim.fn.isdirectory(entry.path) == 1 then - table.insert(files, entry.path) - end + -- Validate that the path exists + if vim.fn.filereadable(real_path) == 1 or vim.fn.isdirectory(real_path) == 1 then + table.insert(files, real_path) end end + end - if #files > 0 then - return files, nil - end + if #files > 0 then + return files, nil else - -- Normal mode: get file under cursor - local entry_ok, entry = pcall(mini_files.get_fs_entry) - if not entry_ok or not entry then - return {}, "Failed to get entry from mini.files" + return {}, "No files found in range" + end +end + +function M._get_mini_files_selection() + local success, mini_files = pcall(require, "mini.files") + if not success then + return {}, "mini.files not available" + end + + local files = {} + + local bufnr = vim.api.nvim_get_current_buf() + + -- Normal mode: get file under cursor + local entry_ok, entry = pcall(mini_files.get_fs_entry, bufnr) + if not entry_ok or not entry then + return {}, "Failed to get entry from mini.files" + end + + if entry.path and entry.path ~= "" then + -- Extract real filesystem path from mini.files buffer path + local real_path = entry.path + -- Remove mini.files buffer protocol prefix if present + if real_path:match("^minifiles://") then + real_path = real_path:gsub("^minifiles://[^/]*/", "") end - if entry.path and entry.path ~= "" then - -- Validate that the path exists - if vim.fn.filereadable(entry.path) == 1 or vim.fn.isdirectory(entry.path) == 1 then - return { entry.path }, nil - else - return {}, "Invalid file or directory path: " .. entry.path - end + -- Validate that the path exists + if vim.fn.filereadable(real_path) == 1 or vim.fn.isdirectory(real_path) == 1 then + return { real_path }, nil + else + return {}, "Invalid file or directory path: " .. real_path end end diff --git a/tests/unit/mini_files_integration_spec.lua b/tests/unit/mini_files_integration_spec.lua index bd7cb01..a54b4fd 100644 --- a/tests/unit/mini_files_integration_spec.lua +++ b/tests/unit/mini_files_integration_spec.lua @@ -42,6 +42,11 @@ describe("mini.files integration", function() return 0 end, }, + api = { + nvim_get_current_buf = function() + return 1 -- Mock buffer ID + end, + }, bo = { filetype = "minifiles" }, } @@ -57,7 +62,11 @@ describe("mini.files integration", function() it("should get single file under cursor", function() -- Mock mini.files module local mock_mini_files = { - get_fs_entry = function() + get_fs_entry = function(buf_id) + -- Verify buffer ID is passed correctly + if buf_id ~= 1 then + return nil + end return { path = "/Users/test/project/main.lua" } end, } @@ -74,7 +83,11 @@ describe("mini.files integration", function() it("should get directory under cursor", function() -- Mock mini.files module local mock_mini_files = { - get_fs_entry = function() + get_fs_entry = function(buf_id) + -- Verify buffer ID is passed correctly + if buf_id ~= 1 then + return nil + end return { path = "/Users/test/project/src" } end, } @@ -88,22 +101,14 @@ describe("mini.files integration", function() expect(files[1]).to_be("/Users/test/project/src") end) - it("should get multiple files in visual mode", function() - mock_vim.fn.mode = function() - return "V" -- Visual line mode - end - - -- Mock mini.files module + it("should handle mini.files buffer path format", function() + -- Mock mini.files module that returns buffer-style paths local mock_mini_files = { - get_fs_entry = function(buf_id, line) - if line == 1 then - return { path = "/Users/test/project/file1.lua" } - elseif line == 2 then - return { path = "/Users/test/project/file2.lua" } - elseif line == 3 then - return { path = "/Users/test/project/src" } + get_fs_entry = function(buf_id) + if buf_id ~= 1 then + return nil end - return nil + return { path = "minifiles://42//Users/test/project/buffer_file.lua" } end, } package.loaded["mini.files"] = mock_mini_files @@ -112,39 +117,37 @@ describe("mini.files integration", function() expect(err).to_be_nil() expect(files).to_be_table() - expect(#files).to_be(3) - expect(files[1]).to_be("/Users/test/project/file1.lua") - expect(files[2]).to_be("/Users/test/project/file2.lua") - expect(files[3]).to_be("/Users/test/project/src") + expect(#files).to_be(1) + expect(files[1]).to_be("/Users/test/project/buffer_file.lua") end) - it("should filter out invalid files in visual mode", function() - mock_vim.fn.mode = function() - return "V" -- Visual line mode - end - - -- Mock mini.files module - local mock_mini_files = { - get_fs_entry = function(buf_id, line) - if line == 1 then - return { path = "/Users/test/project/valid.lua" } - elseif line == 2 then - return { path = "/Users/test/project/invalid.xyz" } -- Won't pass filereadable/isdirectory - elseif line == 3 then - return { path = "/Users/test/project/src" } - end - return nil - end, + it("should handle various mini.files buffer path formats", function() + -- Test different buffer path formats that could occur + local test_cases = { + { input = "minifiles://42/Users/test/file.lua", expected = "Users/test/file.lua" }, + { input = "minifiles://42//Users/test/file.lua", expected = "/Users/test/file.lua" }, + { input = "minifiles://123///Users/test/file.lua", expected = "//Users/test/file.lua" }, + { input = "/Users/test/normal_path.lua", expected = "/Users/test/normal_path.lua" }, } - package.loaded["mini.files"] = mock_mini_files - - local files, err = integrations._get_mini_files_selection() - expect(err).to_be_nil() - expect(files).to_be_table() - expect(#files).to_be(2) -- Only valid.lua and src - expect(files[1]).to_be("/Users/test/project/valid.lua") - expect(files[2]).to_be("/Users/test/project/src") + for i, test_case in ipairs(test_cases) do + local mock_mini_files = { + get_fs_entry = function(buf_id) + if buf_id ~= 1 then + return nil + end + return { path = test_case.input } + end, + } + package.loaded["mini.files"] = mock_mini_files + + local files, err = integrations._get_mini_files_selection() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(1) + expect(files[1]).to_be(test_case.expected) + end end) it("should handle empty entry under cursor", function() @@ -228,26 +231,6 @@ describe("mini.files integration", function() expect(files).to_be_table() expect(#files).to_be(0) end) - - it("should handle visual mode with no valid entries", function() - mock_vim.fn.mode = function() - return "V" -- Visual line mode - end - - -- Mock mini.files module - local mock_mini_files = { - get_fs_entry = function(buf_id, line) - return nil -- No entries - end, - } - package.loaded["mini.files"] = mock_mini_files - - local files, err = integrations._get_mini_files_selection() - - expect(err).to_be("No file found under cursor") - expect(files).to_be_table() - expect(#files).to_be(0) - end) end) describe("get_selected_files_from_tree", function() @@ -279,4 +262,4 @@ describe("mini.files integration", function() expect(files).to_be_nil() end) end) -end) \ No newline at end of file +end) From 6e99e1939ecc77cd2974f974d220cf0aaeec0a41 Mon Sep 17 00:00:00 2001 From: Jim Durand Date: Tue, 29 Jul 2025 09:29:22 -0400 Subject: [PATCH 4/5] feat: add test fixture for mini.files --- ARCHITECTURE.md | 1 + DEVELOPMENT.md | 1 + fixtures/mini-files/init.lua | 1 + fixtures/mini-files/lazy-lock.json | 5 + fixtures/mini-files/lua/config/lazy.lua | 41 ++++ .../mini-files/lua/plugins/dev-claudecode.lua | 84 +++++++++ fixtures/mini-files/lua/plugins/init.lua | 12 ++ .../mini-files/lua/plugins/mini-files.lua | 176 ++++++++++++++++++ 8 files changed, 321 insertions(+) create mode 100644 fixtures/mini-files/init.lua create mode 100644 fixtures/mini-files/lazy-lock.json create mode 100644 fixtures/mini-files/lua/config/lazy.lua create mode 100644 fixtures/mini-files/lua/plugins/dev-claudecode.lua create mode 100644 fixtures/mini-files/lua/plugins/init.lua create mode 100644 fixtures/mini-files/lua/plugins/mini-files.lua diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c3ce317..4874ae6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -246,6 +246,7 @@ source fixtures/nvim-aliases.sh vv nvim-tree # Test with nvim-tree integration vv oil # Test with oil.nvim integration vv netrw # Test with built-in netrw +vv mini-files # Test with built-in mini.files # Each fixture provides: # - Complete Neovim configuration diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index de17b42..4e4b85b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,6 +9,7 @@ claudecode.nvim/ ├── .github/workflows/ # CI workflow definitions ├── fixtures/ # Test Neovim configurations for integration testing │ ├── bin/ # Helper scripts (vv, vve, list-configs) +│ ├── mini-files/ # Neovim config testing with mini.files │ ├── netrw/ # Neovim config testing with built-in file explorer │ ├── nvim-tree/ # Neovim config testing with nvim-tree.lua │ ├── oil/ # Neovim config testing with oil.nvim diff --git a/fixtures/mini-files/init.lua b/fixtures/mini-files/init.lua new file mode 100644 index 0000000..55b8979 --- /dev/null +++ b/fixtures/mini-files/init.lua @@ -0,0 +1 @@ +require("config.lazy") diff --git a/fixtures/mini-files/lazy-lock.json b/fixtures/mini-files/lazy-lock.json new file mode 100644 index 0000000..b8b4afd --- /dev/null +++ b/fixtures/mini-files/lazy-lock.json @@ -0,0 +1,5 @@ +{ + "lazy.nvim": { "branch": "main", "commit": "6c3bda4aca61a13a9c63f1c1d1b16b9d3be90d7a" }, + "mini.files": { "branch": "main", "commit": "5b9431cf5c69b8e69e5a67d2d12338a3ac2e1541" }, + "tokyonight.nvim": { "branch": "main", "commit": "057ef5d260c1931f1dffd0f052c685dcd14100a3" } +} diff --git a/fixtures/mini-files/lua/config/lazy.lua b/fixtures/mini-files/lua/config/lazy.lua new file mode 100644 index 0000000..2d86d18 --- /dev/null +++ b/fixtures/mini-files/lua/config/lazy.lua @@ -0,0 +1,41 @@ +-- Bootstrap lazy.nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local lazyrepo = "https://github.com/folke/lazy.nvim.git" + local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) + if vim.v.shell_error ~= 0 then + vim.api.nvim_echo({ + { "Failed to clone lazy.nvim:\n", "ErrorMsg" }, + { out, "WarningMsg" }, + { "\nPress any key to exit..." }, + }, true, {}) + vim.fn.getchar() + os.exit(1) + end +end +vim.opt.rtp:prepend(lazypath) + +-- Make sure to setup `mapleader` and `maplocalleader` before +-- loading lazy.nvim so that mappings are correct. +-- This is also a good place to setup other settings (vim.opt) +vim.g.mapleader = " " +vim.g.maplocalleader = "\\" + +-- Setup lazy.nvim +require("lazy").setup({ + spec = { + -- import your plugins + { import = "plugins" }, + }, + -- Configure any other settings here. See the documentation for more details. + -- colorscheme that will be used when installing plugins. + install = { colorscheme = { "habamax" } }, + -- automatically check for plugin updates + checker = { enabled = true }, +}) + +-- Add keybind for Lazy plugin manager +vim.keymap.set("n", "l", "Lazy", { desc = "Lazy Plugin Manager" }) + +-- Terminal keybindings +vim.keymap.set("t", "", "", { desc = "Exit terminal mode (double esc)" }) diff --git a/fixtures/mini-files/lua/plugins/dev-claudecode.lua b/fixtures/mini-files/lua/plugins/dev-claudecode.lua new file mode 100644 index 0000000..ab532fb --- /dev/null +++ b/fixtures/mini-files/lua/plugins/dev-claudecode.lua @@ -0,0 +1,84 @@ +-- Development configuration for claudecode.nvim +-- This is Thomas's personal config for developing claudecode.nvim +-- Symlink this to your personal Neovim config: +-- ln -s ~/projects/claudecode.nvim/dev-config.lua ~/.config/nvim/lua/plugins/dev-claudecode.lua + +return { + "coder/claudecode.nvim", + dev = true, -- Use local development version + keys = { + -- AI/Claude Code prefix + { "a", nil, desc = "AI/Claude Code" }, + + -- Core Claude commands + { "ac", "ClaudeCode", desc = "Toggle Claude" }, + { "af", "ClaudeCodeFocus", desc = "Focus Claude" }, + { "ar", "ClaudeCode --resume", desc = "Resume Claude" }, + { "aC", "ClaudeCode --continue", desc = "Continue Claude" }, + { "am", "ClaudeCodeSelectModel", desc = "Select Claude model" }, + + -- Context sending + { "ab", "ClaudeCodeAdd %", desc = "Add current buffer" }, + { "as", "ClaudeCodeSend", mode = "v", desc = "Send to Claude" }, + { + "as", + "ClaudeCodeTreeAdd", + desc = "Add file from tree", + ft = { "NvimTree", "neo-tree", "oil", "minifiles" }, + }, + + -- Development helpers + { "ao", "ClaudeCodeOpen", desc = "Open Claude" }, + { "aq", "ClaudeCodeClose", desc = "Close Claude" }, + { "ai", "ClaudeCodeStatus", desc = "Claude Status" }, + { "aS", "ClaudeCodeStart", desc = "Start Claude Server" }, + { "aQ", "ClaudeCodeStop", desc = "Stop Claude Server" }, + + -- Diff management (buffer-local, only active in diff buffers) + { "aa", "ClaudeCodeDiffAccept", desc = "Accept diff" }, + { "ad", "ClaudeCodeDiffDeny", desc = "Deny diff" }, + }, + + -- Development configuration - all options shown with defaults commented out + opts = { + -- Server Configuration + -- port_range = { min = 10000, max = 65535 }, -- WebSocket server port range + -- auto_start = true, -- Auto-start server on Neovim startup + -- log_level = "info", -- "trace", "debug", "info", "warn", "error" + -- terminal_cmd = nil, -- Custom terminal command (default: "claude") + + -- Selection Tracking + -- track_selection = true, -- Enable real-time selection tracking + -- visual_demotion_delay_ms = 50, -- Delay before demoting visual selection (ms) + + -- Connection Management + -- connection_wait_delay = 200, -- Wait time after connection before sending queued @ mentions (ms) + -- connection_timeout = 10000, -- Max time to wait for Claude Code connection (ms) + -- queue_timeout = 5000, -- Max time to keep @ mentions in queue (ms) + + -- Diff Integration + -- diff_opts = { + -- auto_close_on_accept = true, -- Close diff view after accepting changes + -- show_diff_stats = true, -- Show diff statistics + -- vertical_split = true, -- Use vertical split for diffs + -- open_in_current_tab = true, -- Open diffs in current tab vs new tab + -- }, + + -- Terminal Configuration + -- terminal = { + -- split_side = "right", -- "left" or "right" + -- split_width_percentage = 0.30, -- Width as percentage (0.0 to 1.0) + -- provider = "auto", -- "auto", "snacks", or "native" + -- show_native_term_exit_tip = true, -- Show exit tip for native terminal + -- auto_close = true, -- Auto-close terminal after command completion + -- snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()` + -- }, + + -- Development overrides (uncomment as needed) + -- log_level = "debug", + -- terminal = { + -- provider = "native", + -- auto_close = false, -- Keep terminals open to see output + -- }, + }, +} diff --git a/fixtures/mini-files/lua/plugins/init.lua b/fixtures/mini-files/lua/plugins/init.lua new file mode 100644 index 0000000..e911afe --- /dev/null +++ b/fixtures/mini-files/lua/plugins/init.lua @@ -0,0 +1,12 @@ +-- Basic plugin configuration +return { + -- Example: add a colorscheme + { + "folke/tokyonight.nvim", + lazy = false, + priority = 1000, + config = function() + vim.cmd([[colorscheme tokyonight]]) + end, + }, +} diff --git a/fixtures/mini-files/lua/plugins/mini-files.lua b/fixtures/mini-files/lua/plugins/mini-files.lua new file mode 100644 index 0000000..e69748e --- /dev/null +++ b/fixtures/mini-files/lua/plugins/mini-files.lua @@ -0,0 +1,176 @@ +return { + "echasnovski/mini.files", + version = false, + config = function() + require("mini.files").setup({ + -- Customization of shown content + content = { + -- Predicate for which file system entries to show + filter = nil, + -- What prefix to show to the left of file system entry + prefix = nil, + -- In which order to show file system entries + sort = nil, + }, + + -- Module mappings created only inside explorer. + -- Use `''` (empty string) to not create one. + mappings = { + close = "q", + go_in = "l", + go_in_plus = "L", + go_out = "h", + go_out_plus = "H", + reset = "", + reveal_cwd = "@", + show_help = "g?", + synchronize = "=", + trim_left = "<", + trim_right = ">", + }, + + -- General options + options = { + -- Whether to delete permanently or move into module-specific trash + permanent_delete = true, + -- Whether to use for editing directories + use_as_default_explorer = true, + }, + + -- Customization of explorer windows + windows = { + -- Maximum number of windows to show side by side + max_number = math.huge, + -- Whether to show preview of file/directory under cursor + preview = false, + -- Width of focused window + width_focus = 50, + -- Width of non-focused window + width_nofocus = 15, + -- Width of preview window + width_preview = 25, + }, + }) + + -- Global keybindings for mini.files + vim.keymap.set("n", "e", function() + require("mini.files").open() + end, { desc = "Open mini.files (current dir)" }) + + vim.keymap.set("n", "E", function() + require("mini.files").open(vim.api.nvim_buf_get_name(0)) + end, { desc = "Open mini.files (current file)" }) + + vim.keymap.set("n", "-", function() + require("mini.files").open() + end, { desc = "Open parent directory" }) + + -- Mini.files specific keybindings and autocommands + vim.api.nvim_create_autocmd("User", { + pattern = "MiniFilesBufferCreate", + callback = function(args) + local buf_id = args.data.buf_id + + -- Add buffer-local keybindings + vim.keymap.set("n", "", function() + -- Split window and open file + local cur_target = require("mini.files").get_fs_entry() + if cur_target and cur_target.fs_type == "file" then + require("mini.files").close() + vim.cmd("split " .. cur_target.path) + end + end, { buffer = buf_id, desc = "Split and open file" }) + + vim.keymap.set("n", "", function() + -- Vertical split and open file + local cur_target = require("mini.files").get_fs_entry() + if cur_target and cur_target.fs_type == "file" then + require("mini.files").close() + vim.cmd("vsplit " .. cur_target.path) + end + end, { buffer = buf_id, desc = "Vertical split and open file" }) + + vim.keymap.set("n", "", function() + -- Open in new tab + local cur_target = require("mini.files").get_fs_entry() + if cur_target and cur_target.fs_type == "file" then + require("mini.files").close() + vim.cmd("tabnew " .. cur_target.path) + end + end, { buffer = buf_id, desc = "Open in new tab" }) + + -- Create new file/directory + vim.keymap.set("n", "a", function() + local cur_target = require("mini.files").get_fs_entry() + local path = cur_target and cur_target.path or require("mini.files").get_explorer_state().cwd + local new_name = vim.fn.input("Create: " .. path .. "/") + if new_name and new_name ~= "" then + if new_name:sub(-1) == "/" then + -- Create directory + vim.fn.mkdir(path .. "/" .. new_name, "p") + else + -- Create file + local new_file = io.open(path .. "/" .. new_name, "w") + if new_file then + new_file:close() + end + end + require("mini.files").refresh() + end + end, { buffer = buf_id, desc = "Create new file/directory" }) + + -- Rename file/directory + vim.keymap.set("n", "r", function() + local cur_target = require("mini.files").get_fs_entry() + if cur_target then + local old_name = vim.fn.fnamemodify(cur_target.path, ":t") + local new_name = vim.fn.input("Rename to: ", old_name) + if new_name and new_name ~= "" and new_name ~= old_name then + local new_path = vim.fn.fnamemodify(cur_target.path, ":h") .. "/" .. new_name + os.rename(cur_target.path, new_path) + require("mini.files").refresh() + end + end + end, { buffer = buf_id, desc = "Rename file/directory" }) + + -- Delete file/directory + vim.keymap.set("n", "d", function() + local cur_target = require("mini.files").get_fs_entry() + if cur_target then + local confirm = vim.fn.confirm("Delete " .. cur_target.path .. "?", "&Yes\n&No", 2) + if confirm == 1 then + if cur_target.fs_type == "directory" then + vim.fn.delete(cur_target.path, "rf") + else + vim.fn.delete(cur_target.path) + end + require("mini.files").refresh() + end + end + end, { buffer = buf_id, desc = "Delete file/directory" }) + end, + }) + + -- Auto-close mini.files when it's the last window + vim.api.nvim_create_autocmd("User", { + pattern = "MiniFilesBufferUpdate", + callback = function() + if vim.bo.filetype == "minifiles" then + -- Check if this is the only window left + local windows = vim.api.nvim_list_wins() + local minifiles_windows = 0 + for _, win in ipairs(windows) do + local buf = vim.api.nvim_win_get_buf(win) + if vim.api.nvim_buf_get_option(buf, "filetype") == "minifiles" then + minifiles_windows = minifiles_windows + 1 + end + end + + if #windows == minifiles_windows then + vim.cmd("quit") + end + end + end, + }) + end, +} From 32eaa98bc1b99fd705a79f12b3906455614c1b4c Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 29 Jul 2025 17:11:21 +0200 Subject: [PATCH 5/5] refactor: replace duplicated dev config with symlink and remove unused variable Change-Id: I277a88d244cb84f517a4b899293a00408293041f Signed-off-by: Thomas Kosiewski --- .../mini-files/lua/plugins/dev-claudecode.lua | 85 +------------------ lua/claudecode/integrations.lua | 2 - 2 files changed, 1 insertion(+), 86 deletions(-) mode change 100644 => 120000 fixtures/mini-files/lua/plugins/dev-claudecode.lua diff --git a/fixtures/mini-files/lua/plugins/dev-claudecode.lua b/fixtures/mini-files/lua/plugins/dev-claudecode.lua deleted file mode 100644 index ab532fb..0000000 --- a/fixtures/mini-files/lua/plugins/dev-claudecode.lua +++ /dev/null @@ -1,84 +0,0 @@ --- Development configuration for claudecode.nvim --- This is Thomas's personal config for developing claudecode.nvim --- Symlink this to your personal Neovim config: --- ln -s ~/projects/claudecode.nvim/dev-config.lua ~/.config/nvim/lua/plugins/dev-claudecode.lua - -return { - "coder/claudecode.nvim", - dev = true, -- Use local development version - keys = { - -- AI/Claude Code prefix - { "a", nil, desc = "AI/Claude Code" }, - - -- Core Claude commands - { "ac", "ClaudeCode", desc = "Toggle Claude" }, - { "af", "ClaudeCodeFocus", desc = "Focus Claude" }, - { "ar", "ClaudeCode --resume", desc = "Resume Claude" }, - { "aC", "ClaudeCode --continue", desc = "Continue Claude" }, - { "am", "ClaudeCodeSelectModel", desc = "Select Claude model" }, - - -- Context sending - { "ab", "ClaudeCodeAdd %", desc = "Add current buffer" }, - { "as", "ClaudeCodeSend", mode = "v", desc = "Send to Claude" }, - { - "as", - "ClaudeCodeTreeAdd", - desc = "Add file from tree", - ft = { "NvimTree", "neo-tree", "oil", "minifiles" }, - }, - - -- Development helpers - { "ao", "ClaudeCodeOpen", desc = "Open Claude" }, - { "aq", "ClaudeCodeClose", desc = "Close Claude" }, - { "ai", "ClaudeCodeStatus", desc = "Claude Status" }, - { "aS", "ClaudeCodeStart", desc = "Start Claude Server" }, - { "aQ", "ClaudeCodeStop", desc = "Stop Claude Server" }, - - -- Diff management (buffer-local, only active in diff buffers) - { "aa", "ClaudeCodeDiffAccept", desc = "Accept diff" }, - { "ad", "ClaudeCodeDiffDeny", desc = "Deny diff" }, - }, - - -- Development configuration - all options shown with defaults commented out - opts = { - -- Server Configuration - -- port_range = { min = 10000, max = 65535 }, -- WebSocket server port range - -- auto_start = true, -- Auto-start server on Neovim startup - -- log_level = "info", -- "trace", "debug", "info", "warn", "error" - -- terminal_cmd = nil, -- Custom terminal command (default: "claude") - - -- Selection Tracking - -- track_selection = true, -- Enable real-time selection tracking - -- visual_demotion_delay_ms = 50, -- Delay before demoting visual selection (ms) - - -- Connection Management - -- connection_wait_delay = 200, -- Wait time after connection before sending queued @ mentions (ms) - -- connection_timeout = 10000, -- Max time to wait for Claude Code connection (ms) - -- queue_timeout = 5000, -- Max time to keep @ mentions in queue (ms) - - -- Diff Integration - -- diff_opts = { - -- auto_close_on_accept = true, -- Close diff view after accepting changes - -- show_diff_stats = true, -- Show diff statistics - -- vertical_split = true, -- Use vertical split for diffs - -- open_in_current_tab = true, -- Open diffs in current tab vs new tab - -- }, - - -- Terminal Configuration - -- terminal = { - -- split_side = "right", -- "left" or "right" - -- split_width_percentage = 0.30, -- Width as percentage (0.0 to 1.0) - -- provider = "auto", -- "auto", "snacks", or "native" - -- show_native_term_exit_tip = true, -- Show exit tip for native terminal - -- auto_close = true, -- Auto-close terminal after command completion - -- snacks_win_opts = {}, -- Opts to pass to `Snacks.terminal.open()` - -- }, - - -- Development overrides (uncomment as needed) - -- log_level = "debug", - -- terminal = { - -- provider = "native", - -- auto_close = false, -- Keep terminals open to see output - -- }, - }, -} diff --git a/fixtures/mini-files/lua/plugins/dev-claudecode.lua b/fixtures/mini-files/lua/plugins/dev-claudecode.lua new file mode 120000 index 0000000..f609a1c --- /dev/null +++ b/fixtures/mini-files/lua/plugins/dev-claudecode.lua @@ -0,0 +1 @@ +../../../../dev-config.lua \ No newline at end of file diff --git a/lua/claudecode/integrations.lua b/lua/claudecode/integrations.lua index 759c5d6..c4646a8 100644 --- a/lua/claudecode/integrations.lua +++ b/lua/claudecode/integrations.lua @@ -311,8 +311,6 @@ function M._get_mini_files_selection() return {}, "mini.files not available" end - local files = {} - local bufnr = vim.api.nvim_get_current_buf() -- Normal mode: get file under cursor