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