From 847c45b786488aac38561359e51b292c34668bde Mon Sep 17 00:00:00 2001 From: Jumpei Yamakawa Date: Mon, 9 Jun 2025 21:35:42 +0900 Subject: [PATCH 1/4] feat: add oil.nvim support for file selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add oil.nvim integration to support file selection with @-mention - Support both visual selection and single file under cursor - Handle directories, files, and symbolic links properly - Add comprehensive unit tests for oil.nvim integration - Update README documentation to include oil.nvim support 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 6 +- lua/claudecode/integrations.lua | 87 ++++++++++- lua/claudecode/visual_commands.lua | 36 +++++ tests/unit/oil_integration_spec.lua | 229 ++++++++++++++++++++++++++++ 4 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 tests/unit/oil_integration_spec.lua 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/integrations.lua b/lua/claudecode/integrations.lua index f5adeff..22bab0b 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,87 @@ 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/visual_commands.lua b/lua/claudecode/visual_commands.lua index 4e76c41..795ecc1 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,35 @@ 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 + local integrations = require("claudecode.integrations") + -- 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..ea11eae --- /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) \ No newline at end of file From 3a8bb5c11c91b0b890dd87dfd60d7418ba80317a Mon Sep 17 00:00:00 2001 From: Jumpei Yamakawa Date: Mon, 9 Jun 2025 21:52:47 +0900 Subject: [PATCH 2/4] fix: add missing oil.nvim filetype checks for window selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add oil filetype check in diff.lua's _find_main_editor_window() - Add oil filetype check in open_file.lua's find_main_editor_window() - Add oil filetype check in init.lua's ClaudeCodeSend command These changes ensure oil.nvim buffers are properly excluded when searching for main editor windows, preventing diff views and file opens from appearing in the oil explorer window. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lua/claudecode/diff.lua | 1 + lua/claudecode/init.lua | 1 + lua/claudecode/tools/open_file.lua | 1 + 3 files changed, 3 insertions(+) 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/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" ) From b4f8f0187a2b083c79c74f3179a774b5c3270559 Mon Sep 17 00:00:00 2001 From: Jumpei Yamakawa Date: Mon, 16 Jun 2025 01:19:19 +0900 Subject: [PATCH 3/4] fix: remove unnecessary variable --- lua/claudecode/visual_commands.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/claudecode/visual_commands.lua b/lua/claudecode/visual_commands.lua index 795ecc1..4056728 100644 --- a/lua/claudecode/visual_commands.lua +++ b/lua/claudecode/visual_commands.lua @@ -352,7 +352,6 @@ function M.get_files_from_visual_selection(visual_data) -- Get current directory once local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr) if dir_ok and current_dir then - local integrations = require("claudecode.integrations") -- 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) From 74a7ef3e06cdacee3c6dafccd6a9f8e5e5ec5426 Mon Sep 17 00:00:00 2001 From: Jumpei Yamakawa Date: Mon, 16 Jun 2025 01:25:45 +0900 Subject: [PATCH 4/4] fix: run formatter --- lua/claudecode/integrations.lua | 2 -- tests/unit/oil_integration_spec.lua | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/claudecode/integrations.lua b/lua/claudecode/integrations.lua index 22bab0b..2827aab 100644 --- a/lua/claudecode/integrations.lua +++ b/lua/claudecode/integrations.lua @@ -180,8 +180,6 @@ 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 diff --git a/tests/unit/oil_integration_spec.lua b/tests/unit/oil_integration_spec.lua index ea11eae..235e7b0 100644 --- a/tests/unit/oil_integration_spec.lua +++ b/tests/unit/oil_integration_spec.lua @@ -226,4 +226,4 @@ describe("oil.nvim integration", function() expect(files[1]).to_be("/path/test.lua") end) end) -end) \ No newline at end of file +end)