Skip to content

feat: add nvim-tree and neotree integration #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim):
keys = {
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
-- Tree integration works automatically with the same keybinding
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", desc = "Add file to Claude context", ft = { "NvimTree", "neo-tree" } },
},
}
```
Expand All @@ -60,13 +62,37 @@ That's it! For more configuration options, see [Advanced Setup](#advanced-setup)
## Usage

1. **Launch Claude**: Run `:ClaudeCode` to open Claude in a split terminal
2. **Send context**: Select text and run `:'<,'>ClaudeCodeSend` to send it to Claude
2. **Send context**:
- Select text in visual mode and use `<leader>as` to send it to Claude
- In `nvim-tree` or `neo-tree`, press `<leader>as` on a file to add it to Claude's context
3. **Let Claude work**: Claude can now:
- See your current file and selections in real-time
- Open files in your editor
- Show diffs with proposed changes
- Access diagnostics and workspace info

## Commands

- `:ClaudeCode` - Toggle the Claude Code terminal window
- `:ClaudeCodeSend` - Send current visual selection to Claude, or add files from tree explorer
- `:ClaudeCodeTreeAdd` - Add selected file(s) from tree explorer to Claude context (also available via ClaudeCodeSend)

### Tree Integration

The `<leader>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

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 `<leader>as`
- **Multiple files**: Select multiple files (using tree plugin's selection features) and press `<leader>as`
- **Smart detection**: Automatically detects whether you're in nvim-tree or neo-tree
- **Error handling**: Clear feedback if no files are selected or if tree plugins aren't available

## How It Works

This plugin creates a WebSocket server that Claude Code CLI connects to, implementing the same protocol as the official VS Code extension. When you launch Claude, it automatically detects Neovim and gains full access to your editor.
Expand Down
111 changes: 110 additions & 1 deletion lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,68 @@ function M._create_commands()
vim.notify("Claude Code integration is not running", vim.log.levels.ERROR)
return
end

-- Check if we're in a tree buffer - if so, delegate to tree integration
local current_ft = vim.bo.filetype
local current_bufname = vim.api.nvim_buf_get_name(0)
logger.debug(
"command",
"ClaudeCodeSend: Buffer detection - filetype: '" .. current_ft .. "', bufname: '" .. current_bufname .. "'"
)

-- Check both filetype and buffer name for tree detection
local is_tree_buffer = current_ft == "NvimTree"
or current_ft == "neo-tree"
or string.match(current_bufname, "neo%-tree")
or string.match(current_bufname, "NvimTree")

if is_tree_buffer then
logger.debug("command", "ClaudeCodeSend: Detected tree buffer, delegating to tree integration")

local integrations = require("claudecode.integrations")
local files, error = integrations.get_selected_files_from_tree()

if error then
logger.warn("command", "ClaudeCodeSend->TreeAdd: " .. error)
return
end

if not files or #files == 0 then
logger.warn("command", "ClaudeCodeSend->TreeAdd: No files selected")
return
end

-- Send each file as an at_mention (full file, no line numbers)
local success_count = 0
for _, file_path in ipairs(files) do
local params = {
filePath = file_path,
lineStart = nil, -- No line numbers for full file
lineEnd = nil, -- No line numbers for full file
}

local broadcast_success = M.state.server.broadcast("at_mentioned", params)
if broadcast_success then
success_count = success_count + 1
logger.debug("command", "ClaudeCodeSend->TreeAdd: Added file " .. file_path)
else
logger.error("command", "ClaudeCodeSend->TreeAdd: Failed to add file " .. file_path)
end
end

if success_count > 0 then
local message = success_count == 1 and "Added 1 file to Claude context"
or string.format("Added %d files to Claude context", success_count)
logger.debug("command", message) -- Use debug level to avoid popup
else
logger.error("command", "ClaudeCodeSend->TreeAdd: Failed to add any files")
end

-- Exit visual mode if we were in it
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
return
end

logger.debug(
"command",
"ClaudeCodeSend (new logic) invoked. Mode: "
Expand All @@ -263,7 +325,6 @@ function M._create_commands()

if not M.state.server then
logger.error("command", "ClaudeCodeSend: Claude Code integration is not running.")
vim.notify("Claude Code integration is not running", vim.log.levels.ERROR, { title = "ClaudeCode Error" })
return
end

Expand Down Expand Up @@ -292,6 +353,54 @@ function M._create_commands()
range = true, -- Important: This makes the command expect a range (visual selection)
})

vim.api.nvim_create_user_command("ClaudeCodeTreeAdd", function()
if not M.state.server then
logger.error("command", "ClaudeCodeTreeAdd: Claude Code integration is not running.")
return
end

local integrations = require("claudecode.integrations")
local files, error = integrations.get_selected_files_from_tree()

if error then
logger.warn("command", "ClaudeCodeTreeAdd: " .. error)
return
end

if not files or #files == 0 then
logger.warn("command", "ClaudeCodeTreeAdd: No files selected")
return
end

-- Send each file as an at_mention (full file, no line numbers)
local success_count = 0
for _, file_path in ipairs(files) do
local params = {
filePath = file_path,
lineStart = nil, -- No line numbers for full file
lineEnd = nil, -- No line numbers for full file
}

local broadcast_success = M.state.server.broadcast("at_mentioned", params)
if broadcast_success then
success_count = success_count + 1
logger.debug("command", "ClaudeCodeTreeAdd: Added file " .. file_path)
else
logger.error("command", "ClaudeCodeTreeAdd: Failed to add file " .. file_path)
end
end

if success_count > 0 then
local message = success_count == 1 and "Added 1 file to Claude context"
or string.format("Added %d files to Claude context", success_count)
logger.debug("command", message)
else
logger.error("command", "ClaudeCodeTreeAdd: Failed to add any files")
end
end, {
desc = "Add selected file(s) from tree explorer to Claude Code context",
})

local terminal_ok, terminal = pcall(require, "claudecode.terminal")
if terminal_ok then
vim.api.nvim_create_user_command("ClaudeCode", function(_opts)
Expand Down
186 changes: 186 additions & 0 deletions lua/claudecode/integrations.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
-- Tree integration module for ClaudeCode.nvim
-- Handles detection and selection of files from nvim-tree and neo-tree
-- @module claudecode.integrations
local M = {}

local logger = require("claudecode.logger")

--- Get selected files from the current tree explorer
--- @return table|nil files List of file paths, or nil if error
--- @return string|nil error Error message if operation failed
function M.get_selected_files_from_tree()
local current_ft = vim.bo.filetype

if current_ft == "NvimTree" then
return M._get_nvim_tree_selection()
elseif current_ft == "neo-tree" then
return M._get_neotree_selection()
else
return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")"
end
end

--- Get selected files from nvim-tree
--- Supports both multi-selection (marks) and single file under cursor
--- @return table files List of file paths
--- @return string|nil error Error message if operation failed
function M._get_nvim_tree_selection()
local success, nvim_tree_api = pcall(require, "nvim-tree.api")
if not success then
logger.warn("integrations", "nvim-tree API not available")
return {}, "nvim-tree not available"
end

local files = {}

-- Check for multi-selection first (marked files)
local marks = nvim_tree_api.marks.list()
if marks and #marks > 0 then
logger.debug("integrations", "Found " .. #marks .. " marked files in nvim-tree")
for _, mark in ipairs(marks) do
if mark.type == "file" and mark.absolute_path then
table.insert(files, mark.absolute_path)
logger.debug("integrations", "Added marked file: " .. mark.absolute_path)
end
end
if #files > 0 then
return files, nil
end
end

-- Fall back to node under cursor
local node = nvim_tree_api.tree.get_node_under_cursor()
if node then
if node.type == "file" and node.absolute_path then
logger.debug("integrations", "Found file under cursor: " .. node.absolute_path)
return { node.absolute_path }, nil
elseif node.type == "directory" then
return {}, "Cannot add directory to Claude context. Please select a file."
end
end

return {}, "No file found under cursor"
end

--- Get selected files from neo-tree
--- Uses neo-tree's own visual selection method when in visual mode
--- @return table files List of file paths
--- @return string|nil error Error message if operation failed
function M._get_neotree_selection()
local success, manager = pcall(require, "neo-tree.sources.manager")
if not success then
logger.warn("integrations", "neo-tree manager not available")
return {}, "neo-tree not available"
end

local state = manager.get_state("filesystem")
if not state then
logger.warn("integrations", "neo-tree filesystem state not available")
return {}, "neo-tree filesystem state not available"
end

local files = {}

-- Method 1: Use neo-tree's own visual selection method (like their copy/paste feature)
-- This matches how neo-tree internally handles visual selection
local mode = vim.fn.mode()
if mode == "V" or mode == "v" or mode == "\22" then -- Visual modes
logger.debug("integrations", "Visual mode detected: " .. mode)

-- Check if we're in the neo-tree window
if state.winid and state.winid == vim.api.nvim_get_current_win() then
logger.debug("integrations", "In neo-tree window, getting visual selection using neo-tree method")

-- Use neo-tree's method to get selected nodes from visual range
local start_pos = vim.fn.getpos("'<")[2]
local end_pos = vim.fn.getpos("'>")[2]

if end_pos < start_pos then
start_pos, end_pos = end_pos, start_pos
end

logger.debug("integrations", "Visual selection from line " .. start_pos .. " to " .. end_pos)

local selected_nodes = {}
for line = start_pos, end_pos do
local node = state.tree:get_node(line)
if node then
table.insert(selected_nodes, node)
logger.debug(
"integrations",
"Found node at line " .. line .. ": type=" .. (node.type or "nil") .. ", path=" .. (node.path or "nil")
)
else
logger.debug("integrations", "No node found at line " .. line)
end
end

logger.debug("integrations", "Found " .. #selected_nodes .. " selected nodes from visual range")

-- Extract file paths from selected nodes
for _, node in ipairs(selected_nodes) do
if node.type == "file" and node.path then
table.insert(files, node.path)
logger.debug("integrations", "Added file from visual selection: " .. node.path)
end
end

if #files > 0 then
return files, nil
else
logger.debug("integrations", "No files found in visual selection (" .. #selected_nodes .. " nodes total)")
end
else
logger.debug("integrations", "Not in neo-tree window, visual selection method not applicable")
end
end

-- Method 2: Try neo-tree's built-in selection methods
if state.tree then
local selection = nil

if state.tree.get_selection then
selection = state.tree:get_selection()
if selection and #selection > 0 then
logger.debug("integrations", "Found selection via get_selection: " .. #selection)
end
end

-- Method 3: Check state-level selection
if (not selection or #selection == 0) and state.selected_nodes then
selection = state.selected_nodes
logger.debug("integrations", "Found selection via state.selected_nodes: " .. #selection)
end

-- Process selection if found
if selection and #selection > 0 then
for _, node in ipairs(selection) do
if node.type == "file" and node.path then
table.insert(files, node.path)
logger.debug("integrations", "Added selected file: " .. node.path)
end
end
if #files > 0 then
return files, nil
end
end
end

-- Fall back to current node under cursor
if state.tree then
local node = state.tree:get_node()
if node then
if node.type == "file" and node.path then
logger.debug("integrations", "Found file under cursor: " .. node.path)
return { node.path }, nil
elseif node.type == "directory" then
return {}, "Cannot add directory to Claude context. Please select a file."
end
end
end

return {}, "No file found under cursor"
end

return M
Loading