Skip to content
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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) •
Expand Down Expand Up @@ -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 = {
Expand Down
70 changes: 68 additions & 2 deletions lua/claude-code/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
257 changes: 257 additions & 0 deletions lua/claude-code/floating.lua
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +148 to +168
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Shell-command construction is vulnerable to spaces / quoting

Building cmd with string concatenation will break when git_root or config.command contain spaces or shell-metacharacters and can even be exploited if filenames are crafted maliciously. Please quote/escape the segments or, better, use vim.fn.termopen({cmd, arg1, …}, opts) with the list-form to avoid a shell entirely.

🤖 Prompt for AI Agents
In lua/claude-code/floating.lua around lines 148 to 168, the construction of the
shell command string using concatenation is unsafe because it does not handle
spaces or special shell characters in git_root or config.command. To fix this,
avoid building a single shell command string; instead, use vim.fn.termopen with
a list of command and arguments to bypass the shell and handle escaping
automatically. Refactor the code to pass the command and its arguments as a list
rather than a concatenated string.


-- 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,
})
Comment on lines +189 to +197
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

once = true prevents cleanup on subsequent windows

The WinClosed autocommand fires only for the first window created from this buffer; when the user re-opens and later manually closes the floating window again, winid is left stale, causing is_open() to mis-report until the next toggle. Drop once = true or recreate the autocommand each time you open a new window.

🤖 Prompt for AI Agents
In lua/claude-code/floating.lua around lines 189 to 197, the WinClosed
autocommand uses once = true, which causes it to run only once and not clean up
the winid on subsequent window closures. To fix this, remove the once = true
option so the autocommand triggers every time the floating window is closed,
ensuring the winid is properly cleared each time.


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