diff --git a/README.md b/README.md index ad28941..9f742fd 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,15 @@ That's it! For more configuration options, see [Advanced Setup](#advanced-setup) The `as` keybinding has context-aware behavior: - **In normal buffers (visual mode)**: Sends selected text to Claude -- **In nvim-tree/neo-tree buffers**: Adds the file under cursor (or selected files) to Claude's context +- **In nvim-tree/neo-tree/oil.nvim buffers**: Adds the file under cursor (or selected files) to Claude's context This allows you to quickly add entire files to Claude's context for review, refactoring, or discussion. #### Features - **Single file**: Place cursor on any file and press `as` -- **Multiple files**: Select multiple files (using tree plugin's selection features) and press `as` -- **Smart detection**: Automatically detects whether you're in nvim-tree or neo-tree +- **Multiple files**: Select multiple files (using tree plugin's selection features or visual selection in oil.nvim) and press `as` +- **Smart detection**: Automatically detects whether you're in nvim-tree, neo-tree, or oil.nvim - **Error handling**: Clear feedback if no files are selected or if tree plugins aren't available ### Direct File Addition diff --git a/lua/claudecode/diff.lua b/lua/claudecode/diff.lua index ef44503..c16d65e 100644 --- a/lua/claudecode/diff.lua +++ b/lua/claudecode/diff.lua @@ -49,6 +49,7 @@ function M._find_main_editor_window() or filetype == "neo-tree-popup" or filetype == "ClaudeCode" or filetype == "NvimTree" + or filetype == "oil" or filetype == "aerial" or filetype == "tagbar" ) diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index 2900099..2db3eb0 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -408,6 +408,7 @@ function M._create_commands() local is_tree_buffer = current_ft == "NvimTree" or current_ft == "neo-tree" + or current_ft == "oil" or string.match(current_bufname, "neo%-tree") or string.match(current_bufname, "NvimTree") diff --git a/lua/claudecode/integrations.lua b/lua/claudecode/integrations.lua index f5adeff..2827aab 100644 --- a/lua/claudecode/integrations.lua +++ b/lua/claudecode/integrations.lua @@ -1,6 +1,6 @@ --- -- Tree integration module for ClaudeCode.nvim --- Handles detection and selection of files from nvim-tree and neo-tree +-- Handles detection and selection of files from nvim-tree, neo-tree, and oil.nvim -- @module claudecode.integrations local M = {} @@ -14,6 +14,8 @@ function M.get_selected_files_from_tree() return M._get_nvim_tree_selection() elseif current_ft == "neo-tree" then return M._get_neotree_selection() + elseif current_ft == "oil" then + return M._get_oil_selection() else return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")" end @@ -178,4 +180,85 @@ function M._get_neotree_selection() return {}, "No file found under cursor" end +--- Get selected files from oil.nvim +--- Supports both visual selection and single file under cursor +--- @return table files List of file paths +--- @return string|nil error Error message if operation failed +function M._get_oil_selection() + local success, oil = pcall(require, "oil") + if not success then + return {}, "oil.nvim not available" + end + + local bufnr = vim.api.nvim_get_current_buf() --[[@as number]] + local files = {} + + -- Check if we're in visual mode + local mode = vim.fn.mode() + if mode == "V" or mode == "v" or mode == "\22" then + -- Visual mode: use the common visual range function + local visual_commands = require("claudecode.visual_commands") + local start_line, end_line = visual_commands.get_visual_range() + + -- Get current directory once + local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr) + if not dir_ok or not current_dir then + return {}, "Failed to get current directory" + end + + -- Process each line in the visual selection + for line = start_line, end_line do + local entry_ok, entry = pcall(oil.get_entry_on_line, bufnr, line) + if entry_ok and entry and entry.name then + -- Skip parent directory entries + if entry.name ~= ".." and entry.name ~= "." then + local full_path = current_dir .. entry.name + -- Handle various entry types + if entry.type == "file" or entry.type == "link" then + table.insert(files, full_path) + elseif entry.type == "directory" then + -- Ensure directory paths end with / + table.insert(files, full_path:match("/$") and full_path or full_path .. "/") + else + -- For unknown types, return the path anyway + table.insert(files, full_path) + end + end + end + end + + if #files > 0 then + return files, nil + end + else + -- Normal mode: get file under cursor with error handling + local ok, entry = pcall(oil.get_cursor_entry) + if not ok or not entry then + return {}, "Failed to get cursor entry" + end + + local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr) + if not dir_ok or not current_dir then + return {}, "Failed to get current directory" + end + + -- Process the entry + if entry.name and entry.name ~= ".." and entry.name ~= "." then + local full_path = current_dir .. entry.name + -- Handle various entry types + if entry.type == "file" or entry.type == "link" then + return { full_path }, nil + elseif entry.type == "directory" then + -- Ensure directory paths end with / + return { full_path:match("/$") and full_path or full_path .. "/" }, nil + else + -- For unknown types, return the path anyway + return { full_path }, nil + end + end + end + + return {}, "No file found under cursor" +end + return M diff --git a/lua/claudecode/tools/open_file.lua b/lua/claudecode/tools/open_file.lua index 49a777c..855a28b 100644 --- a/lua/claudecode/tools/open_file.lua +++ b/lua/claudecode/tools/open_file.lua @@ -75,6 +75,7 @@ local function find_main_editor_window() or filetype == "neo-tree-popup" or filetype == "ClaudeCode" or filetype == "NvimTree" + or filetype == "oil" or filetype == "aerial" or filetype == "tagbar" ) diff --git a/lua/claudecode/visual_commands.lua b/lua/claudecode/visual_commands.lua index 4e76c41..4056728 100644 --- a/lua/claudecode/visual_commands.lua +++ b/lua/claudecode/visual_commands.lua @@ -166,6 +166,13 @@ function M.get_tree_state() end return nvim_tree_api, "nvim-tree" + elseif current_ft == "oil" then + local oil_success, oil = pcall(require, "oil") + if not oil_success then + return nil, nil + end + + return oil, "oil" else return nil, nil end @@ -338,6 +345,34 @@ function M.get_files_from_visual_selection(visual_data) end end files = unique_files + elseif tree_type == "oil" then + local oil = tree_state + local bufnr = vim.api.nvim_get_current_buf() + + -- Get current directory once + local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr) + if dir_ok and current_dir then + -- Access the process_oil_entry function through a module method + for line = start_pos, end_pos do + local entry_ok, entry = pcall(oil.get_entry_on_line, bufnr, line) + if entry_ok and entry and entry.name then + -- Skip parent directory entries + if entry.name ~= ".." and entry.name ~= "." then + local full_path = current_dir .. entry.name + -- Handle various entry types + if entry.type == "file" or entry.type == "link" then + table.insert(files, full_path) + elseif entry.type == "directory" then + -- Ensure directory paths end with / + table.insert(files, full_path:match("/$") and full_path or full_path .. "/") + else + -- For unknown types, return the path anyway + table.insert(files, full_path) + end + end + end + end + end end return files, nil diff --git a/tests/unit/oil_integration_spec.lua b/tests/unit/oil_integration_spec.lua new file mode 100644 index 0000000..235e7b0 --- /dev/null +++ b/tests/unit/oil_integration_spec.lua @@ -0,0 +1,229 @@ +-- luacheck: globals expect +require("tests.busted_setup") + +describe("oil.nvim integration", function() + local integrations + local mock_vim + + local function setup_mocks() + package.loaded["claudecode.integrations"] = nil + package.loaded["claudecode.visual_commands"] = nil + package.loaded["claudecode.logger"] = nil + + -- Mock logger + package.loaded["claudecode.logger"] = { + debug = function() end, + warn = function() end, + error = function() end, + } + + mock_vim = { + fn = { + mode = function() + return "n" -- Default to normal mode + end, + line = function(mark) + if mark == "'<" then + return 2 + elseif mark == "'>" then + return 4 + end + return 1 + end, + }, + api = { + nvim_get_current_buf = function() + return 1 + end, + nvim_win_get_cursor = function() + return { 4, 0 } + end, + nvim_get_mode = function() + return { mode = "n" } + end, + }, + bo = { filetype = "oil" }, + } + + _G.vim = mock_vim + end + + before_each(function() + setup_mocks() + integrations = require("claudecode.integrations") + end) + + describe("_get_oil_selection", function() + it("should get single file under cursor in normal mode", function() + local mock_oil = { + get_cursor_entry = function() + return { type = "file", name = "main.lua" } + end, + get_current_dir = function(bufnr) + return "/Users/test/project/" + end, + } + + package.loaded["oil"] = mock_oil + + local files, err = integrations._get_oil_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 in normal mode", function() + local mock_oil = { + get_cursor_entry = function() + return { type = "directory", name = "src" } + end, + get_current_dir = function(bufnr) + return "/Users/test/project/" + end, + } + + package.loaded["oil"] = mock_oil + + local files, err = integrations._get_oil_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 skip parent directory entries", function() + local mock_oil = { + get_cursor_entry = function() + return { type = "directory", name = ".." } + end, + get_current_dir = function(bufnr) + return "/Users/test/project/" + end, + } + + package.loaded["oil"] = mock_oil + + local files, err = integrations._get_oil_selection() + + expect(err).to_be("No file found under cursor") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle symbolic links", function() + local mock_oil = { + get_cursor_entry = function() + return { type = "link", name = "linked_file.lua" } + end, + get_current_dir = function(bufnr) + return "/Users/test/project/" + end, + } + + package.loaded["oil"] = mock_oil + + local files, err = integrations._get_oil_selection() + + expect(err).to_be_nil() + expect(files).to_be_table() + expect(#files).to_be(1) + expect(files[1]).to_be("/Users/test/project/linked_file.lua") + end) + + it("should handle visual mode selection", function() + -- Mock visual mode + mock_vim.fn.mode = function() + return "V" + end + mock_vim.api.nvim_get_mode = function() + return { mode = "V" } + end + + -- Mock visual_commands module + package.loaded["claudecode.visual_commands"] = { + get_visual_range = function() + return 2, 4 -- Lines 2 to 4 + end, + } + + local line_entries = { + [2] = { type = "file", name = "file1.lua" }, + [3] = { type = "directory", name = "src" }, + [4] = { type = "file", name = "file2.lua" }, + } + + local mock_oil = { + get_current_dir = function(bufnr) + return "/Users/test/project/" + end, + get_entry_on_line = function(bufnr, line) + return line_entries[line] + end, + } + + package.loaded["oil"] = mock_oil + + local files, err = integrations._get_oil_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/src/") + expect(files[3]).to_be("/Users/test/project/file2.lua") + end) + + it("should handle errors gracefully", function() + local mock_oil = { + get_cursor_entry = function() + error("Failed to get cursor entry") + end, + } + + package.loaded["oil"] = mock_oil + + local files, err = integrations._get_oil_selection() + + expect(err).to_be("Failed to get cursor entry") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + + it("should handle missing oil.nvim", function() + package.loaded["oil"] = nil + + local files, err = integrations._get_oil_selection() + + expect(err).to_be("oil.nvim not available") + expect(files).to_be_table() + expect(#files).to_be(0) + end) + end) + + describe("get_selected_files_from_tree", function() + it("should detect oil filetype and delegate to _get_oil_selection", function() + mock_vim.bo.filetype = "oil" + + local mock_oil = { + get_cursor_entry = function() + return { type = "file", name = "test.lua" } + end, + get_current_dir = function(bufnr) + return "/path/" + end, + } + + package.loaded["oil"] = mock_oil + + 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) + end) +end)