Skip to content

feat: add nvim-tree and neotree integration #22

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 4 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
.PHONY: check format test clean

# Default target
all: check format
all: format check test

# Check for syntax errors
check:
@echo "Checking Lua files for syntax errors..."
@find lua -name "*.lua" -type f -exec lua -e "assert(loadfile('{}'))" \;
nix develop .#ci -c find lua -name "*.lua" -type f -exec lua -e "assert(loadfile('{}'))" \;
@echo "Running luacheck..."
@luacheck lua/ tests/ --no-unused-args --no-max-line-length
nix develop .#ci -c luacheck lua/ tests/ --no-unused-args --no-max-line-length

# Format all files
format:
@echo "Formatting files..."
@if command -v nix >/dev/null 2>&1; then \
nix fmt; \
elif command -v stylua >/dev/null 2>&1; then \
stylua lua/; \
else \
echo "Neither nix nor stylua found. Please install one of them."; \
exit 1; \
fi
nix fmt

# Run tests
test:
Expand Down
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim):
"coder/claudecode.nvim",
config = true,
keys = {
{ "<leader>a", nil, desc = "AI/Claude Code" },
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
{
"<leader>as",
"<cmd>ClaudeCodeTreeAdd<cr>",
desc = "Add file",
ft = { "NvimTree", "neo-tree" },
},
},
}
```
Expand All @@ -60,13 +67,80 @@ 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)
- `:ClaudeCodeAdd <file-path> [start-line] [end-line]` - Add a specific file or directory to Claude context by path with optional line range

### 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

### Direct File Addition

The `:ClaudeCodeAdd` command allows you to add files or directories directly by path, with optional line range specification:

```vim
:ClaudeCodeAdd src/main.lua
:ClaudeCodeAdd ~/projects/myproject/
:ClaudeCodeAdd ./README.md
:ClaudeCodeAdd src/main.lua 50 100 " Lines 50-100 only
:ClaudeCodeAdd config.lua 25 " From line 25 to end of file
```

#### Features

- **Path completion**: Tab completion for file and directory paths
- **Path expansion**: Supports `~` for home directory and relative paths
- **Line range support**: Optionally specify start and end lines for files (ignored for directories)
- **Validation**: Checks that files and directories exist before adding, validates line numbers
- **Flexible**: Works with both individual files and entire directories

#### Examples

```vim
" Add entire files
:ClaudeCodeAdd src/components/Header.tsx
:ClaudeCodeAdd ~/.config/nvim/init.lua
" Add entire directories (line numbers ignored)
:ClaudeCodeAdd tests/
:ClaudeCodeAdd ../other-project/
" Add specific line ranges
:ClaudeCodeAdd src/main.lua 50 100 " Lines 50 through 100
:ClaudeCodeAdd config.lua 25 " From line 25 to end of file
:ClaudeCodeAdd utils.py 1 50 " First 50 lines
:ClaudeCodeAdd README.md 10 20 " Just lines 10-20
" Path expansion works with line ranges
:ClaudeCodeAdd ~/project/src/app.js 100 200
:ClaudeCodeAdd ./relative/path.lua 30
```

## 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 Expand Up @@ -132,8 +206,15 @@ See [DEVELOPMENT.md](./DEVELOPMENT.md) for build instructions and development gu
},
config = true,
keys = {
{ "<leader>a", nil, desc = "AI/Claude Code" },
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
{
"<leader>as",
"<cmd>ClaudeCodeTreeAdd<cr>",
desc = "Add file",
ft = { "NvimTree", "neo-tree" },
},
{ "<leader>ao", "<cmd>ClaudeCodeOpen<cr>", desc = "Open Claude" },
{ "<leader>ax", "<cmd>ClaudeCodeClose<cr>", desc = "Close Claude" },
},
Expand Down
12 changes: 6 additions & 6 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
luajitPackages.luacov
neovim
treefmt.config.build.wrapper
findutils
];

# Development packages (additional tools for development)
Expand All @@ -49,7 +50,7 @@
gnumake
websocat
jq
claude-code
# claude-code
];
in
{
Expand Down
139 changes: 102 additions & 37 deletions lua/claudecode/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,26 @@ function M._apply_accepted_changes(diff_data, final_content)

require("claudecode.logger").debug("diff", "Writing accepted changes to file:", old_file_path)

-- Ensure parent directories exist for new files
if diff_data.is_new_file then
local parent_dir = vim.fn.fnamemodify(old_file_path, ":h")
if parent_dir and parent_dir ~= "" and parent_dir ~= "." then
require("claudecode.logger").debug("diff", "Creating parent directories for new file:", parent_dir)
local mkdir_success, mkdir_err = pcall(vim.fn.mkdir, parent_dir, "p")
if not mkdir_success then
require("claudecode.logger").error(
"diff",
"Failed to create parent directories:",
parent_dir,
"error:",
mkdir_err
)
return
end
require("claudecode.logger").debug("diff", "Successfully created parent directories:", parent_dir)
end
end

-- Write the content to the actual file
local lines = vim.split(final_content, "\n")
local success, err = pcall(vim.fn.writefile, lines, old_file_path)
Expand Down Expand Up @@ -581,8 +601,9 @@ end
-- @param old_file_path string Path to the original file
-- @param new_buffer number New file buffer ID
-- @param tab_name string The diff identifier
-- @param is_new_file boolean Whether this is a new file (doesn't exist yet)
-- @return table Info about the created diff layout
function M._create_diff_view_from_window(target_window, old_file_path, new_buffer, tab_name)
function M._create_diff_view_from_window(target_window, old_file_path, new_buffer, tab_name, is_new_file)
require("claudecode.logger").debug("diff", "Creating diff view from window", target_window)

-- If no target window provided, create a new window in suitable location
Expand All @@ -608,16 +629,36 @@ function M._create_diff_view_from_window(target_window, old_file_path, new_buffe
vim.api.nvim_set_current_win(target_window)
end

-- Make sure the window shows the file we want to diff
-- This handles the case where the buffer exists but isn't in the current window
vim.cmd("edit " .. vim.fn.fnameescape(old_file_path))

-- Store the original buffer for later
local original_buffer = vim.api.nvim_win_get_buf(target_window)
-- Handle the left side of the diff (original file or empty for new files)
local original_buffer
if is_new_file then
-- Create an empty buffer for new file comparison
require("claudecode.logger").debug("diff", "Creating empty buffer for new file diff")
local empty_buffer = vim.api.nvim_create_buf(false, true) -- unlisted, scratch
vim.api.nvim_buf_set_name(empty_buffer, old_file_path .. " (NEW FILE)")
vim.api.nvim_buf_set_lines(empty_buffer, 0, -1, false, {}) -- Empty content
vim.api.nvim_buf_set_option(empty_buffer, "buftype", "nofile")
vim.api.nvim_buf_set_option(empty_buffer, "modifiable", false)
vim.api.nvim_buf_set_option(empty_buffer, "readonly", true)

-- Set the empty buffer in the target window
vim.api.nvim_win_set_buf(target_window, empty_buffer)
original_buffer = empty_buffer
else
-- Make sure the window shows the existing file we want to diff
vim.cmd("edit " .. vim.fn.fnameescape(old_file_path))
original_buffer = vim.api.nvim_win_get_buf(target_window)
end

-- Enable diff mode on the original file
-- Enable diff mode on the original/empty file
vim.cmd("diffthis")
require("claudecode.logger").debug("diff", "Enabled diff mode on original file in window", target_window)
require("claudecode.logger").debug(
"diff",
"Enabled diff mode on",
is_new_file and "empty buffer" or "original file",
"in window",
target_window
)

-- Create vertical split for new buffer (proposed changes)
vim.cmd("vsplit")
Expand Down Expand Up @@ -647,6 +688,14 @@ function M._create_diff_view_from_window(target_window, old_file_path, new_buffe
-- Accept all changes
local new_content = vim.api.nvim_buf_get_lines(new_buffer, 0, -1, false)

-- Ensure parent directories exist for new files
if is_new_file then
local parent_dir = vim.fn.fnamemodify(old_file_path, ":h")
if parent_dir and parent_dir ~= "" and parent_dir ~= "." then
vim.fn.mkdir(parent_dir, "p")
end
end

-- Write to file
vim.fn.writefile(new_content, old_file_path)

Expand Down Expand Up @@ -747,41 +796,49 @@ function M._setup_blocking_diff(params, resolution_callback)
params.old_file_path
)

-- Step 1: Check if the file exists
-- Step 1: Check if the file exists (allow new files)
local old_file_exists = vim.fn.filereadable(params.old_file_path) == 1
if not old_file_exists then
error({
code = -32000,
message = "File access error",
data = "Cannot open file: " .. params.old_file_path .. " (file does not exist)",
})
end
local is_new_file = not old_file_exists

-- Step 2: Find if the file is already open in a buffer
require("claudecode.logger").debug(
"diff",
"File existence check - old_file_exists:",
old_file_exists,
"is_new_file:",
is_new_file,
"path:",
params.old_file_path
)

-- Step 2: Find if the file is already open in a buffer (only for existing files)
local existing_buffer = nil
local target_window = nil

-- Look for existing buffer with this file
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then
local buf_name = vim.api.nvim_buf_get_name(buf)
if buf_name == params.old_file_path then
existing_buffer = buf
require("claudecode.logger").debug("diff", "Found existing buffer", buf, "for file", params.old_file_path)
break
if old_file_exists then
-- Look for existing buffer with this file
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then
local buf_name = vim.api.nvim_buf_get_name(buf)
if buf_name == params.old_file_path then
existing_buffer = buf
require("claudecode.logger").debug("diff", "Found existing buffer", buf, "for file", params.old_file_path)
break
end
end
end
end

-- Find window containing this buffer (if any)
if existing_buffer then
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_get_buf(win) == existing_buffer then
target_window = win
require("claudecode.logger").debug("diff", "Found window", win, "containing buffer", existing_buffer)
break
-- Find window containing this buffer (if any)
if existing_buffer then
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_get_buf(win) == existing_buffer then
target_window = win
require("claudecode.logger").debug("diff", "Found window", win, "containing buffer", existing_buffer)
break
end
end
end
else
require("claudecode.logger").debug("diff", "Skipping buffer search for new file:", params.old_file_path)
end

-- If no existing buffer/window, find a suitable main editor window
Expand Down Expand Up @@ -811,7 +868,7 @@ function M._setup_blocking_diff(params, resolution_callback)
})
end

local new_unique_name = tab_name .. " (proposed)"
local new_unique_name = is_new_file and (tab_name .. " (NEW FILE - proposed)") or (tab_name .. " (proposed)")
vim.api.nvim_buf_set_name(new_buffer, new_unique_name)
vim.api.nvim_buf_set_lines(new_buffer, 0, -1, false, vim.split(params.new_file_contents, "\n"))

Expand All @@ -820,8 +877,15 @@ function M._setup_blocking_diff(params, resolution_callback)
vim.api.nvim_buf_set_option(new_buffer, "modifiable", true)

-- Step 4: Set up diff view using the target window
require("claudecode.logger").debug("diff", "Creating diff view from window", target_window)
local diff_info = M._create_diff_view_from_window(target_window, params.old_file_path, new_buffer, tab_name)
require("claudecode.logger").debug(
"diff",
"Creating diff view from window",
target_window,
"is_new_file:",
is_new_file
)
local diff_info =
M._create_diff_view_from_window(target_window, params.old_file_path, new_buffer, tab_name, is_new_file)

-- Step 5: Register autocmds for user interaction monitoring
require("claudecode.logger").debug("diff", "Registering autocmds")
Expand All @@ -842,6 +906,7 @@ function M._setup_blocking_diff(params, resolution_callback)
status = "pending",
resolution_callback = resolution_callback,
result_content = nil,
is_new_file = is_new_file,
})
require("claudecode.logger").debug("diff", "Setup completed successfully for", tab_name)
end
Expand Down
Loading
Loading