diff --git a/README.md b/README.md index d398883..56a0db2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim): keys = { { "ac", "ClaudeCode", desc = "Toggle Claude" }, { "as", "ClaudeCodeSend", mode = "v", desc = "Send to Claude" }, + -- Tree integration works automatically with the same keybinding + { "as", "ClaudeCodeSend", desc = "Add file to Claude context", ft = { "NvimTree", "neo-tree" } }, }, } ``` @@ -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 `as` to send it to Claude + - In `nvim-tree` or `neo-tree`, press `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 `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 `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 +- **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. diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index 489785c..079d633 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -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("", true, false, true), "n", false) + return + end + logger.debug( "command", "ClaudeCodeSend (new logic) invoked. Mode: " @@ -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 @@ -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) diff --git a/lua/claudecode/integrations.lua b/lua/claudecode/integrations.lua new file mode 100644 index 0000000..0538396 --- /dev/null +++ b/lua/claudecode/integrations.lua @@ -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 diff --git a/lua/claudecode/integrations.lua.backup b/lua/claudecode/integrations.lua.backup new file mode 100644 index 0000000..fe61197 --- /dev/null +++ b/lua/claudecode/integrations.lua.backup @@ -0,0 +1,230 @@ +--- +-- 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 +--- Supports both multi-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_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 = {} + local selection = nil + + -- Debug: Log available state structure + logger.debug("integrations", "neo-tree state available, checking for selection") + + -- Method 1: Check for visual selection in neo-tree (when using V to select multiple lines) + -- This is likely what happens when you select multiple files with visual mode + + -- Get visual selection range if in visual mode + local mode = vim.fn.mode() + if mode == "V" or mode == "v" or mode == "\22" then -- Visual modes + logger.debug("integrations", "Visual mode detected: " .. mode) + + -- Get the visual selection range + local start_line = vim.fn.line("v") + local end_line = vim.fn.line(".") + if start_line > end_line then + start_line, end_line = end_line, start_line + end + + logger.debug("integrations", "Visual selection from line " .. start_line .. " to " .. end_line) + + -- Get the rendered tree to map line numbers to file paths + if state.tree and state.tree.get_nodes then + local nodes = state.tree:get_nodes() + if nodes then + logger.debug("integrations", "Found " .. #nodes .. " top-level nodes") + local line_to_node = {} + + -- Build a mapping of line numbers to nodes + local function map_nodes(node_list, depth) + depth = depth or 0 + for i, node in ipairs(node_list) do + logger.debug( + "integrations", + "Node " + .. i + .. " at depth " + .. depth + .. ": type=" + .. (node.type or "nil") + .. ", path=" + .. (node.path or "nil") + .. ", position=" + .. (node.position and node.position.row or "nil") + ) + + if node.position and node.position.row then + line_to_node[node.position.row] = node + logger.debug( + "integrations", + "Mapped line " .. node.position.row .. " to node: " .. (node.path or node.name or "unknown") + ) + end + + if node.children and #node.children > 0 then + logger.debug("integrations", "Node has " .. #node.children .. " children") + map_nodes(node.children, depth + 1) + end + end + end + + map_nodes(nodes) + + logger.debug("integrations", "Built line_to_node mapping with " .. vim.tbl_count(line_to_node) .. " entries") + + -- Debug: Show what's in the line mapping for our selection range + for line = start_line, end_line do + local node = line_to_node[line] + if node then + logger.debug( + "integrations", + "Line " .. line .. " maps to: type=" .. (node.type or "nil") .. ", path=" .. (node.path or "nil") + ) + else + logger.debug("integrations", "Line " .. line .. " has no mapping") + end + end + + -- Get files from selected lines + for line = start_line, end_line do + local node = line_to_node[line] + if node and node.type == "file" and node.path then + table.insert(files, node.path) + logger.debug("integrations", "Added file from line " .. line .. ": " .. node.path) + end + end + + if #files > 0 then + return files, nil + else + logger.debug("integrations", "No files found from visual selection lines " .. start_line .. "-" .. end_line) + end + else + logger.debug("integrations", "get_nodes() returned nil") + end + else + logger.debug("integrations", "state.tree.get_nodes not available") + end + end + + -- Method 2: Try neo-tree's built-in selection methods + if state.tree then + 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