From 51915b4a847ef449c207f1530fe209af55a77eea Mon Sep 17 00:00:00 2001 From: Valentin Dosimont Date: Tue, 3 Jun 2025 06:31:23 +0200 Subject: [PATCH 1/5] feat: add configurable shell command separator --- lua/claude-code/config.lua | 18 ++++++++++++++++++ lua/claude-code/terminal.lua | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lua/claude-code/config.lua b/lua/claude-code/config.lua index ab1d618..5f42184 100644 --- a/lua/claude-code/config.lua +++ b/lua/claude-code/config.lua @@ -47,11 +47,16 @@ local M = {} -- @field verbose string|boolean Enable verbose logging with full turn-by-turn output -- Additional options can be added as needed +--- ClaudeCodeShell class for shell configuration +-- @table ClaudeCodeShell +-- @field separator string Command separator used in shell commands (e.g., '&&', ';', '|') + --- ClaudeCodeConfig class for main configuration -- @table ClaudeCodeConfig -- @field window ClaudeCodeWindow Terminal window settings -- @field refresh ClaudeCodeRefresh File refresh settings -- @field git ClaudeCodeGit Git integration settings +-- @field shell ClaudeCodeShell Shell-specific configuration -- @field command string Command used to launch Claude Code -- @field command_variants ClaudeCodeCommandVariants Command variants configuration -- @field keymaps ClaudeCodeKeymaps Keymaps configuration @@ -81,6 +86,10 @@ M.default_config = { use_git_root = true, -- Set CWD to git root when opening Claude Code (if in git project) multi_instance = true, -- Use multiple Claude instances (one per git root) }, + -- Shell-specific settings + shell = { + separator = '&&', -- Command separator used in shell commands + }, -- Command settings command = 'claude', -- Command used to launch Claude Code -- Command variants @@ -179,6 +188,15 @@ local function validate_config(config) return false, 'git.multi_instance must be a boolean' end + -- Validate shell settings + if type(config.shell) ~= 'table' then + return false, 'shell config must be a table' + end + + if type(config.shell.separator) ~= 'string' then + return false, 'shell.separator must be a string' + end + -- Validate command settings if type(config.command) ~= 'string' then return false, 'command must be a string' diff --git a/lua/claude-code/terminal.lua b/lua/claude-code/terminal.lua index 6adaf16..85c449c 100644 --- a/lua/claude-code/terminal.lua +++ b/lua/claude-code/terminal.lua @@ -149,7 +149,8 @@ function M.toggle(claude_code, config, git) local git_root = git.get_git_root() if git_root then -- Use pushd/popd to change directory instead of --cwd - cmd = 'terminal pushd ' .. git_root .. ' && ' .. config.command .. ' && popd' + local separator = (config.shell and config.shell.separator) or config.shell.separator + cmd = 'terminal pushd ' .. git_root .. ' ' .. separator .. ' ' .. config.command .. ' ' .. separator .. ' popd' end end From 917e18660bda67f2b2007818f4654d325c0372a5 Mon Sep 17 00:00:00 2001 From: Valentin Dosimont Date: Tue, 3 Jun 2025 06:54:14 +0200 Subject: [PATCH 2/5] feat: add configurable shell navigation commands --- README.md | 6 ++++++ lua/claude-code/config.lua | 12 ++++++++++++ lua/claude-code/terminal.lua | 6 ++++-- tests/spec/terminal_spec.lua | 29 +++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36fc78b..c3ff50c 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,12 @@ require("claude-code").setup({ git = { use_git_root = true, -- Set CWD to git root when opening Claude Code (if in git project) }, + -- Shell-specific settings + shell = { + separator = '&&', -- Command separator used in shell commands + pushd_cmd = 'pushd', -- Command to push directory onto stack (e.g., 'pushd' for bash/zsh, 'enter' for nushell) + popd_cmd = 'popd', -- Command to pop directory from stack (e.g., 'popd' for bash/zsh, 'exit' for nushell) + }, -- Command settings command = "claude", -- Command used to launch Claude Code -- Command variants diff --git a/lua/claude-code/config.lua b/lua/claude-code/config.lua index 5f42184..e1c99a8 100644 --- a/lua/claude-code/config.lua +++ b/lua/claude-code/config.lua @@ -50,6 +50,8 @@ local M = {} --- ClaudeCodeShell class for shell configuration -- @table ClaudeCodeShell -- @field separator string Command separator used in shell commands (e.g., '&&', ';', '|') +-- @field pushd_cmd string Command to push directory onto stack (e.g., 'pushd' for bash/zsh) +-- @field popd_cmd string Command to pop directory from stack (e.g., 'popd' for bash/zsh) --- ClaudeCodeConfig class for main configuration -- @table ClaudeCodeConfig @@ -89,6 +91,8 @@ M.default_config = { -- Shell-specific settings shell = { separator = '&&', -- Command separator used in shell commands + pushd_cmd = 'pushd', -- Command to push directory onto stack + popd_cmd = 'popd', -- Command to pop directory from stack }, -- Command settings command = 'claude', -- Command used to launch Claude Code @@ -197,6 +201,14 @@ local function validate_config(config) return false, 'shell.separator must be a string' end + if type(config.shell.pushd_cmd) ~= 'string' then + return false, 'shell.pushd_cmd must be a string' + end + + if type(config.shell.popd_cmd) ~= 'string' then + return false, 'shell.popd_cmd must be a string' + end + -- Validate command settings if type(config.command) ~= 'string' then return false, 'command must be a string' diff --git a/lua/claude-code/terminal.lua b/lua/claude-code/terminal.lua index 85c449c..2b8172d 100644 --- a/lua/claude-code/terminal.lua +++ b/lua/claude-code/terminal.lua @@ -149,8 +149,10 @@ function M.toggle(claude_code, config, git) local git_root = git.get_git_root() if git_root then -- Use pushd/popd to change directory instead of --cwd - local separator = (config.shell and config.shell.separator) or config.shell.separator - cmd = 'terminal pushd ' .. git_root .. ' ' .. separator .. ' ' .. config.command .. ' ' .. separator .. ' popd' + local separator = config.shell.separator + local pushd_cmd = config.shell.pushd_cmd + local popd_cmd = config.shell.popd_cmd + cmd = 'terminal ' .. pushd_cmd .. ' ' .. git_root .. ' ' .. separator .. ' ' .. config.command .. ' ' .. separator .. ' ' .. popd_cmd end end diff --git a/tests/spec/terminal_spec.lua b/tests/spec/terminal_spec.lua index d861c4a..bd5b5fd 100644 --- a/tests/spec/terminal_spec.lua +++ b/tests/spec/terminal_spec.lua @@ -85,6 +85,11 @@ describe('terminal module', function() use_git_root = true, multi_instance = true, }, + shell = { + separator = '&&', + pushd_cmd = 'pushd', + popd_cmd = 'popd', + }, } claude_code = { @@ -300,6 +305,30 @@ describe('terminal module', function() assert.is_true(git_root_cmd_found, 'Terminal command should include git root') end) + + it('should use custom pushd/popd commands when configured', function() + -- Set git config to use root + config.git.use_git_root = true + -- Configure custom directory commands for nushell + config.shell.pushd_cmd = 'enter' + config.shell.popd_cmd = 'exit' + config.shell.separator = ';' + + -- Call toggle + terminal.toggle(claude_code, config, git) + + -- Check that custom commands were used in terminal command + local custom_cmd_found = false + + for _, cmd in ipairs(vim_cmd_calls) do + if cmd:match('terminal enter /test/git/root ; ' .. config.command .. ' ; exit') then + custom_cmd_found = true + break + end + end + + assert.is_true(custom_cmd_found, 'Terminal command should use custom directory commands') + end) end) describe('start_in_normal_mode option', function() From e9cbb1667d435b3fc74b8d0c48c726dff152a560 Mon Sep 17 00:00:00 2001 From: Krishna Balasubramanian Date: Mon, 2 Jun 2025 18:52:34 -0700 Subject: [PATCH 3/5] fix: handle NVIM environment variable in test script When Claude Code runs inside Neovim, the NVIM environment variable points to a socket path instead of the nvim executable. This caused the test script to fail with permission errors. The fix checks if NVIM is an executable file (not a socket) before using it, otherwise falls back to finding nvim in PATH. This allows tests to run properly both from within Claude Code/Neovim and from regular terminals. Added: - Test script to verify NVIM detection logic works correctly - Documentation in DEVELOPMENT.md explaining the behavior --- DEVELOPMENT.md | 14 +++++++++ scripts/test.sh | 17 ++++++---- scripts/test_nvim_detection.sh | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100755 scripts/test_nvim_detection.sh diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bd9cb0f..2531885 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -183,6 +183,20 @@ make test-basic make test-config ``` +### Running Tests from Within Neovim/Claude Code + +When running tests from within a Neovim instance (such as when using Claude Code via claude-code.nvim), the test script automatically handles the `$NVIM` environment variable which normally points to a socket file instead of the nvim executable. + +The test script will: +- Use the `$NVIM` variable if it points to a valid executable file +- Fall back to finding `nvim` in `$PATH` if `$NVIM` points to a socket or invalid path +- Display which nvim binary is being used for transparency + +To verify the NVIM detection logic works correctly, you can run: +```bash +./scripts/test_nvim_detection.sh +``` + ### Writing Tests Tests are written in Lua using a simple BDD-style API: diff --git a/scripts/test.sh b/scripts/test.sh index 529dd6a..4a02fcc 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -12,12 +12,17 @@ cd "$PLUGIN_DIR" # Print current directory for debugging echo "Running tests from: $(pwd)" -# Find nvim -NVIM=${NVIM:-$(which nvim)} - -if [ -z "$NVIM" ]; then - echo "Error: nvim not found in PATH" - exit 1 +# Find nvim - ignore NVIM env var if it points to a socket +if [ -n "$NVIM" ] && [ -x "$NVIM" ] && [ ! -S "$NVIM" ]; then + # NVIM is set and is an executable file (not a socket) + echo "Using NVIM from environment: $NVIM" +else + # Find nvim in PATH + NVIM=$(which nvim) + if [ -z "$NVIM" ]; then + echo "Error: nvim not found in PATH" + exit 1 + fi fi echo "Running tests with $NVIM" diff --git a/scripts/test_nvim_detection.sh b/scripts/test_nvim_detection.sh new file mode 100755 index 0000000..1bc14c3 --- /dev/null +++ b/scripts/test_nvim_detection.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +# Test script to verify NVIM environment variable detection logic +# This script tests the fix for handling NVIM variable when running inside Neovim + +echo "Testing NVIM environment variable detection..." + +# Save original NVIM value +ORIGINAL_NVIM="$NVIM" + +# Test 1: NVIM points to a socket (simulating inside Neovim) +echo "Test 1: NVIM points to a socket" +export NVIM="/tmp/test_socket" +mkfifo "$NVIM" 2>/dev/null || true # Create a named pipe (similar to socket) +if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then + echo "✓ Fallback to PATH works" +else + echo "✗ Fallback failed" +fi +rm -f "$NVIM" + +# Test 2: NVIM points to valid executable +echo "Test 2: NVIM points to valid executable" +export NVIM="$(which nvim)" +if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Using NVIM from environment"; then + echo "✓ Using provided NVIM works" +else + echo "✗ Using provided NVIM failed" +fi + +# Test 3: NVIM points to non-existent path +echo "Test 3: NVIM points to non-existent path" +export NVIM="/nonexistent/nvim" +if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then + echo "✓ Fallback from invalid path works" +else + echo "✗ Fallback from invalid path failed" +fi + +# Test 4: NVIM is unset +echo "Test 4: NVIM is unset" +unset NVIM +if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then + echo "✓ Unset NVIM works" +else + echo "✗ Unset NVIM failed" +fi + +# Restore original NVIM value +if [ -n "$ORIGINAL_NVIM" ]; then + export NVIM="$ORIGINAL_NVIM" +else + unset NVIM +fi + +echo "All NVIM detection tests completed!" \ No newline at end of file From 1010032ae72b6c7ed656af6c2508f216d6382796 Mon Sep 17 00:00:00 2001 From: Krishna Balasubramanian Date: Tue, 3 Jun 2025 16:21:36 -0700 Subject: [PATCH 4/5] Address CodeRabbit feedback: improve script robustness and formatting - Replace 'which' with POSIX-compliant 'command -v' in test scripts - Fix markdown formatting issues in DEVELOPMENT.md (add blank lines) - Separate variable declaration and assignment in test_nvim_detection.sh - Add robust absolute path resolution using readlink and BASH_SOURCE - Enhance error handling with 'set -euo pipefail' for strict mode - Replace unreliable $0 references in bash -c contexts These changes improve portability, reliability, and follow shell scripting best practices while addressing all linting and formatting issues. --- DEVELOPMENT.md | 2 ++ scripts/test.sh | 3 +-- scripts/test_nvim_detection.sh | 20 ++++++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2531885..06bfef7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -188,11 +188,13 @@ make test-config When running tests from within a Neovim instance (such as when using Claude Code via claude-code.nvim), the test script automatically handles the `$NVIM` environment variable which normally points to a socket file instead of the nvim executable. The test script will: + - Use the `$NVIM` variable if it points to a valid executable file - Fall back to finding `nvim` in `$PATH` if `$NVIM` points to a socket or invalid path - Display which nvim binary is being used for transparency To verify the NVIM detection logic works correctly, you can run: + ```bash ./scripts/test_nvim_detection.sh ``` diff --git a/scripts/test.sh b/scripts/test.sh index 4a02fcc..ab2b348 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -18,8 +18,7 @@ if [ -n "$NVIM" ] && [ -x "$NVIM" ] && [ ! -S "$NVIM" ]; then echo "Using NVIM from environment: $NVIM" else # Find nvim in PATH - NVIM=$(which nvim) - if [ -z "$NVIM" ]; then + if ! NVIM=$(command -v nvim); then echo "Error: nvim not found in PATH" exit 1 fi diff --git a/scripts/test_nvim_detection.sh b/scripts/test_nvim_detection.sh index 1bc14c3..5f52349 100755 --- a/scripts/test_nvim_detection.sh +++ b/scripts/test_nvim_detection.sh @@ -1,9 +1,13 @@ #!/bin/bash -set -e +set -euo pipefail # Test script to verify NVIM environment variable detection logic # This script tests the fix for handling NVIM variable when running inside Neovim +# Get the absolute directory path of this script +SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + echo "Testing NVIM environment variable detection..." # Save original NVIM value @@ -13,7 +17,7 @@ ORIGINAL_NVIM="$NVIM" echo "Test 1: NVIM points to a socket" export NVIM="/tmp/test_socket" mkfifo "$NVIM" 2>/dev/null || true # Create a named pipe (similar to socket) -if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then +if timeout 5 bash -c "cd '$PROJECT_DIR' && ./scripts/test.sh" 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then echo "✓ Fallback to PATH works" else echo "✗ Fallback failed" @@ -22,8 +26,12 @@ rm -f "$NVIM" # Test 2: NVIM points to valid executable echo "Test 2: NVIM points to valid executable" -export NVIM="$(which nvim)" -if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Using NVIM from environment"; then +if ! NVIM=$(command -v nvim); then + echo "Error: nvim not found in PATH" + exit 1 +fi +export NVIM +if timeout 5 bash -c "cd '$PROJECT_DIR' && ./scripts/test.sh" 2>&1 | head -10 | grep -q "Using NVIM from environment"; then echo "✓ Using provided NVIM works" else echo "✗ Using provided NVIM failed" @@ -32,7 +40,7 @@ fi # Test 3: NVIM points to non-existent path echo "Test 3: NVIM points to non-existent path" export NVIM="/nonexistent/nvim" -if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then +if timeout 5 bash -c "cd '$PROJECT_DIR' && ./scripts/test.sh" 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then echo "✓ Fallback from invalid path works" else echo "✗ Fallback from invalid path failed" @@ -41,7 +49,7 @@ fi # Test 4: NVIM is unset echo "Test 4: NVIM is unset" unset NVIM -if timeout 5 bash -c 'cd "$(dirname "$0")" && ./scripts/test.sh' 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then +if timeout 5 bash -c "cd '$PROJECT_DIR' && ./scripts/test.sh" 2>&1 | head -10 | grep -q "Running tests with.*nvim"; then echo "✓ Unset NVIM works" else echo "✗ Unset NVIM failed" From 6c2f09109cfa072a5a1fa31c8f3d3dc5895ede0e Mon Sep 17 00:00:00 2001 From: Marco Caceffo Date: Sun, 15 Jun 2025 02:03:36 +0200 Subject: [PATCH 5/5] floating window option --- README.md | 10 +- lua/claude-code/config.lua | 70 +++++++++- lua/claude-code/floating.lua | 257 +++++++++++++++++++++++++++++++++++ lua/claude-code/init.lua | 6 + lua/claude-code/keymaps.lua | 1 + lua/claude-code/terminal.lua | 6 + 6 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 lua/claude-code/floating.lua diff --git a/README.md b/README.md index c3ff50c..43d8ba9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Version](https://img.shields.io/badge/Version-0.4.2-blue?style=flat-square)](https://github.com/greggh/claude-code.nvim/releases/tag/v0.4.2) [![Discussions](https://img.shields.io/github/discussions/greggh/claude-code.nvim?style=flat-square&logo=github)](https://github.com/greggh/claude-code.nvim/discussions) -*A seamless integration between [Claude Code](https://github.com/anthropics/claude-code) AI assistant and Neovim* +_A seamless integration between [Claude Code](https://github.com/anthropics/claude-code) AI assistant and Neovim_ [Features](#features) • [Requirements](#requirements) • @@ -93,10 +93,16 @@ require("claude-code").setup({ -- Terminal window settings window = { split_ratio = 0.3, -- Percentage of screen for the terminal window (height for horizontal, width for vertical splits) - position = "botright", -- Position of the window: "botright", "topleft", "vertical", "rightbelow vsplit", etc. + position = "botright", -- Position of the window: "botright", "topleft", "vertical", "rightbelow vsplit", "floating" etc. enter_insert = true, -- Whether to enter insert mode when opening Claude Code hide_numbers = true, -- Hide line numbers in the terminal window hide_signcolumn = true, -- Hide the sign column in the terminal window + -- Floating window configuration (used when position = "floating") + floating = { + width = 0.8, -- Percentage of screen width for the floating window + height = 0.8, -- Percentage of screen height for the floating window + border = "rounded", -- Border style: "none", "single", "double", "rounded", "solid", "shadow" + }, }, -- File refresh settings refresh = { diff --git a/lua/claude-code/config.lua b/lua/claude-code/config.lua index e1c99a8..12c2db0 100644 --- a/lua/claude-code/config.lua +++ b/lua/claude-code/config.lua @@ -9,11 +9,18 @@ local M = {} --- ClaudeCodeWindow class for window configuration -- @table ClaudeCodeWindow -- @field split_ratio number Percentage of screen for the terminal window (height for horizontal, width for vertical splits) --- @field position string Position of the window: "botright", "topleft", "vertical", etc. +-- @field position string Position of the window: "botright", "topleft", "vertical", "floating", etc. -- @field enter_insert boolean Whether to enter insert mode when opening Claude Code -- @field start_in_normal_mode boolean Whether to start in normal mode instead of insert mode when opening Claude Code -- @field hide_numbers boolean Hide line numbers in the terminal window -- @field hide_signcolumn boolean Hide the sign column in the terminal window +-- @field floating ClaudeCodeFloating Floating window configuration (used when position = "floating") + +--- ClaudeCodeFloating class for floating window configuration +-- @table ClaudeCodeFloating +-- @field width number Percentage of screen width for the floating window (0.0 to 1.0) +-- @field height number Percentage of screen height for the floating window (0.0 to 1.0) +-- @field border string|table Border style for the floating window --- ClaudeCodeRefresh class for file refresh configuration -- @table ClaudeCodeRefresh @@ -70,11 +77,17 @@ M.default_config = { window = { split_ratio = 0.3, -- Percentage of screen for the terminal window (height or width) height_ratio = 0.3, -- DEPRECATED: Use split_ratio instead - position = 'botright', -- Position of the window: "botright", "topleft", "vertical", etc. + position = 'botright', -- Position of the window: "botright", "topleft", "vertical", "floating", etc. enter_insert = true, -- Whether to enter insert mode when opening Claude Code start_in_normal_mode = false, -- Whether to start in normal mode instead of insert mode hide_numbers = true, -- Hide line numbers in the terminal window hide_signcolumn = true, -- Hide the sign column in the terminal window + -- Floating window configuration (used when position = "floating") + floating = { + width = 0.8, -- Percentage of screen width for the floating window + height = 0.8, -- Percentage of screen height for the floating window + border = 'rounded', -- Border style: 'none', 'single', 'double', 'rounded', 'solid', 'shadow' + }, }, -- File refresh settings refresh = { @@ -158,6 +171,38 @@ local function validate_config(config) return false, 'window.hide_signcolumn must be a boolean' end + -- Validate floating window settings if they exist + if config.window.floating then + if type(config.window.floating) ~= 'table' then + return false, 'window.floating config must be a table' + end + + if + type(config.window.floating.width) ~= 'number' + or config.window.floating.width <= 0 + or config.window.floating.width > 1 + then + return false, 'window.floating.width must be a number between 0 and 1' + end + + if + type(config.window.floating.height) ~= 'number' + or config.window.floating.height <= 0 + or config.window.floating.height > 1 + then + return false, 'window.floating.height must be a number between 0 and 1' + end + + if + not ( + type(config.window.floating.border) == 'string' + or type(config.window.floating.border) == 'table' + ) + then + return false, 'window.floating.border must be a string or table' + end + end + -- Validate refresh settings if type(config.refresh) ~= 'table' then return false, 'refresh config must be a table' @@ -292,6 +337,27 @@ function M.parse_config(user_config, silent) end end + -- Handle floating config migration + if user_config and user_config.floating then + -- Migrate old floating config to window.floating + if not user_config.window then + user_config.window = {} + end + if not user_config.window.floating then + user_config.window.floating = user_config.floating + end + -- Remove the old floating config to avoid conflicts + user_config.floating = nil + + -- Show deprecation warning + if not silent then + vim.notify( + 'Claude Code: The floating config has been moved to window.floating. Please update your configuration.', + vim.log.levels.WARN + ) + end + end + local config = vim.tbl_deep_extend('force', {}, M.default_config, user_config or {}) local valid, err = validate_config(config) diff --git a/lua/claude-code/floating.lua b/lua/claude-code/floating.lua new file mode 100644 index 0000000..2e71d62 --- /dev/null +++ b/lua/claude-code/floating.lua @@ -0,0 +1,257 @@ +---@mod claude-code.floating Floating window management for claude-code.nvim +---@brief [[ +--- This module provides floating window functionality for claude-code.nvim. +--- It handles creating, toggling, and managing floating windows. +---@brief ]] + +local M = {} + +--- Floating window state management +-- @table ClaudeCodeFloating +-- @field instances table Key-value store of git root to floating window state +-- @field current_instance string|nil Current git root path for active instance +M.floating = { + instances = {}, + current_instance = nil, +} + +--- Get the current git root or a fallback identifier +--- @param git table The git module +--- @return string identifier Git root path or fallback identifier +local function get_instance_identifier(git) + local git_root = git.get_git_root() + if git_root then + return git_root + else + -- Fallback to current working directory if not in a git repo + return vim.fn.getcwd() + end +end + +--- Calculate floating window dimensions and position +--- @param config table Plugin configuration containing floating window settings +--- @return table window_config Window configuration for nvim_open_win +local function get_window_config(config) + local ui = vim.api.nvim_list_uis()[1] + local floating_config = config.window.floating + local width = math.floor(ui.width * floating_config.width) + local height = math.floor(ui.height * floating_config.height) + + local row = math.floor((ui.height - height) / 2) + local col = math.floor((ui.width - width) / 2) + + return { + relative = 'editor', + width = width, + height = height, + row = row, + col = col, + style = 'minimal', + border = floating_config.border, + title = ' Claude Code ', + title_pos = 'center', + } +end + +--- Create or show floating window +--- @param claude_code table The main plugin module +--- @param config table The plugin configuration +--- @param git table The git module +--- @param existing_bufnr number|nil Buffer number of existing buffer to show in the floating window (optional) +--- @return number bufnr Buffer number of the floating window +--- @return number winid Window ID of the floating window +local function create_floating_window(claude_code, config, git, existing_bufnr) + local win_config = get_window_config(config) + + -- Create buffer if not provided + local bufnr = existing_bufnr + if not bufnr then + bufnr = vim.api.nvim_create_buf(false, true) + end + + -- Create floating window + local winid = vim.api.nvim_open_win(bufnr, true, win_config) + + -- Configure buffer and window options + vim.api.nvim_buf_set_option(bufnr, 'bufhidden', 'hide') + + -- Use window config settings for floating windows + local hide_numbers = config.window.hide_numbers + local hide_signcolumn = config.window.hide_signcolumn + + if hide_numbers then + vim.api.nvim_win_set_option(winid, 'number', false) + vim.api.nvim_win_set_option(winid, 'relativenumber', false) + end + + if hide_signcolumn then + vim.api.nvim_win_set_option(winid, 'signcolumn', 'no') + end + + return bufnr, winid +end + +--- Toggle the Claude Code floating window +--- @param claude_code table The main plugin module +--- @param config table The plugin configuration +--- @param git table The git module +function M.toggle(claude_code, config, git) + -- Determine instance ID based on config + local instance_id + if config.git.multi_instance then + if config.git.use_git_root then + instance_id = get_instance_identifier(git) + else + instance_id = vim.fn.getcwd() + end + else + -- Use a fixed ID for single instance mode + instance_id = 'global' + end + + M.floating.current_instance = instance_id + + -- Check if this floating instance already exists + local floating_state = M.floating.instances[instance_id] + + if floating_state then + local bufnr = floating_state.bufnr + local winid = floating_state.winid + + -- Check if window is still valid and visible + if winid and vim.api.nvim_win_is_valid(winid) then + -- Window is visible, close it + vim.api.nvim_win_close(winid, true) + M.floating.instances[instance_id].winid = nil + return + elseif bufnr and vim.api.nvim_buf_is_valid(bufnr) then + -- Buffer exists but window is closed, recreate window + local new_bufnr, new_winid = create_floating_window(claude_code, config, git, bufnr) + M.floating.instances[instance_id].winid = new_winid + + -- Force insert mode if configured + local enter_insert = config.window.enter_insert + local start_in_normal_mode = config.window.start_in_normal_mode + if enter_insert and not start_in_normal_mode then + vim.schedule(function() + vim.cmd 'startinsert' + end) + end + return + end + end + + -- Create new floating window and terminal + local bufnr, winid = create_floating_window(claude_code, config, git) + + -- Determine terminal command + local cmd = config.command + if config.git and config.git.use_git_root then + local git_root = git.get_git_root() + if git_root then + -- Use pushd/popd to change directory + local separator = config.shell.separator + local pushd_cmd = config.shell.pushd_cmd + local popd_cmd = config.shell.popd_cmd + cmd = pushd_cmd + .. ' ' + .. git_root + .. ' ' + .. separator + .. ' ' + .. config.command + .. ' ' + .. separator + .. ' ' + .. popd_cmd + end + end + + -- Start terminal in the floating window + vim.fn.termopen(cmd) + + -- Create a unique buffer name + local buffer_name + if config.git.multi_instance then + buffer_name = 'claude-code-floating-' .. instance_id:gsub('[^%w%-_]', '-') + else + buffer_name = 'claude-code-floating' + end + vim.api.nvim_buf_set_name(bufnr, buffer_name) + + -- Store the floating window state + M.floating.instances[instance_id] = { + bufnr = bufnr, + winid = winid, + } + + -- Set up window closing autocommand + vim.api.nvim_create_autocmd({ 'WinClosed' }, { + buffer = bufnr, + callback = function() + if M.floating.instances[instance_id] then + M.floating.instances[instance_id].winid = nil + end + end, + once = true, + }) + + -- Automatically enter insert mode if configured + local enter_insert = config.window.enter_insert + local start_in_normal_mode = config.window.start_in_normal_mode + if enter_insert and not start_in_normal_mode then + vim.cmd 'startinsert' + end +end + +--- Close floating window if open +--- @param claude_code table The main plugin module +--- @param config table The plugin configuration +--- @param git table The git module +function M.close(claude_code, config, git) + -- Determine instance ID based on config + local instance_id + if config.git.multi_instance then + if config.git.use_git_root then + instance_id = get_instance_identifier(git) + else + instance_id = vim.fn.getcwd() + end + else + instance_id = 'global' + end + + local floating_state = M.floating.instances[instance_id] + if + floating_state + and floating_state.winid + and vim.api.nvim_win_is_valid(floating_state.winid) + then + vim.api.nvim_win_close(floating_state.winid, true) + M.floating.instances[instance_id].winid = nil + end +end + +--- Check if floating window is currently open +--- @param claude_code table The main plugin module +--- @param config table The plugin configuration +--- @param git table The git module +--- @return boolean is_open Whether the floating window is currently open +function M.is_open(claude_code, config, git) + -- Determine instance ID based on config + local instance_id + if config.git.multi_instance then + if config.git.use_git_root then + instance_id = get_instance_identifier(git) + else + instance_id = vim.fn.getcwd() + end + else + instance_id = 'global' + end + + local floating_state = M.floating.instances[instance_id] + return floating_state and floating_state.winid and vim.api.nvim_win_is_valid(floating_state.winid) +end + +return M diff --git a/lua/claude-code/init.lua b/lua/claude-code/init.lua index 56dee80..4fe45c5 100644 --- a/lua/claude-code/init.lua +++ b/lua/claude-code/init.lua @@ -22,6 +22,7 @@ local commands = require('claude-code.commands') local keymaps = require('claude-code.keymaps') local file_refresh = require('claude-code.file_refresh') local terminal = require('claude-code.terminal') +local floating = require('claude-code.floating') local git = require('claude-code.git') local version = require('claude-code.version') @@ -38,6 +39,10 @@ M.config = {} --- @type table M.claude_code = terminal.terminal +-- Floating window management +--- @type table +M.floating = floating.floating + --- Force insert mode when entering the Claude Code window --- This is a public function used in keymaps function M.force_insert_mode() @@ -94,6 +99,7 @@ function M.toggle_with_variant(variant_name) M.config.command = original_command end + --- Get the current version of the plugin --- @return string version Current version string function M.get_version() diff --git a/lua/claude-code/keymaps.lua b/lua/claude-code/keymaps.lua index 5441bd1..f7b89f8 100644 --- a/lua/claude-code/keymaps.lua +++ b/lua/claude-code/keymaps.lua @@ -22,6 +22,7 @@ function M.register_keymaps(claude_code, config) ) end + if config.keymaps.toggle.terminal then -- Terminal mode toggle keymap -- In terminal mode, special keys like Ctrl need different handling diff --git a/lua/claude-code/terminal.lua b/lua/claude-code/terminal.lua index 2b8172d..dce9407 100644 --- a/lua/claude-code/terminal.lua +++ b/lua/claude-code/terminal.lua @@ -100,6 +100,12 @@ end --- @param config table The plugin configuration --- @param git table The git module function M.toggle(claude_code, config, git) + -- If position is floating, delegate to floating window system + if config.window.position == 'floating' then + local floating = require('claude-code.floating') + return floating.toggle(claude_code, config, git) + end + -- Determine instance ID based on config local instance_id if config.git.multi_instance then