Skip to content

Commit 66077d6

Browse files
committed
feat(terminal): improve visual mode handling and terminal focus logic
Change-Id: I2cfe67e68f381e234a32692e88f9febb7c8007cd Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent f1cad48 commit 66077d6

File tree

3 files changed

+77
-33
lines changed

3 files changed

+77
-33
lines changed

README.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ A Neovim plugin that integrates with Claude Code CLI to provide a seamless AI co
88

99
## Features
1010

11-
- 🔄 **Pure Neovim WebSocket Server** - Zero external dependencies, uses only Neovim built-ins
12-
- 🌐 **RFC 6455 Compliant** - Full WebSocket protocol implementation with JSON-RPC 2.0
11+
- 🔄 **Pure Neovim WebSocket Server** (implemented with Neovim built-ins)
12+
- 🌐 **RFC 6455 Compliant** (WebSocket with JSON-RPC 2.0)
1313
- 🔍 Selection tracking to provide context to Claude
1414
- 🛠️ Integration with Neovim's buffer and window management
1515
- 📝 Support for file operations and diagnostics
@@ -22,7 +22,7 @@ A Neovim plugin that integrates with Claude Code CLI to provide a seamless AI co
2222
- Claude Code CLI installed and in your PATH
2323
- **Optional for terminal integration:** [folke/snacks.nvim](https://github.com/folke/snacks.nvim) - Terminal management plugin (can use native Neovim terminal as an alternative).
2424

25-
**Zero External Dependencies**: The WebSocket server is implemented using pure Neovim built-ins (`vim.loop`, `vim.json`, `vim.schedule`) with no external Lua libraries required.
25+
The WebSocket server uses only Neovim built-ins (`vim.loop`, `vim.json`, `vim.schedule`) for its implementation.
2626

2727
Note: The terminal feature can use `Snacks.nvim` or the native Neovim terminal. If `Snacks.nvim` is configured as the provider but is not available, it will fall back to the native terminal.
2828

@@ -54,10 +54,10 @@ Add the following to your plugins configuration:
5454
config = true,
5555
-- Optional: Add convenient keymaps
5656
keys = {
57-
{ "<leader>cc", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude Terminal" },
58-
{ "<leader>ck", "<cmd>ClaudeCodeSend<cr>", desc = "Send to Claude Code" },
59-
{ "<leader>co", "<cmd>ClaudeCodeOpen<cr>", desc = "Open Claude Terminal" },
60-
{ "<leader>cx", "<cmd>ClaudeCodeClose<cr>", desc = "Close Claude Terminal" },
57+
{ "<leader>ac", "<cmd>ClaudeCode<cr>", mode = { "n", "v" }, desc = "Toggle Claude Terminal" },
58+
{ "<leader>ak", "<cmd>ClaudeCodeSend<cr>", mode = { "n", "v" }, desc = "Send to Claude Code" },
59+
{ "<leader>ao", "<cmd>ClaudeCodeOpen<cr>", mode = { "n", "v" }, desc = "Open/Focus Claude Terminal" },
60+
{ "<leader>ax", "<cmd>ClaudeCodeClose<cr>", mode = { "n", "v" }, desc = "Close Claude Terminal" },
6161
},
6262
}
6363
```
@@ -124,21 +124,21 @@ return {
124124
},
125125
config = true,
126126
keys = {
127-
{ "<leader>cc", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude Terminal" },
128-
{ "<leader>ck", "<cmd>ClaudeCodeSend<cr>", desc = "Send to Claude Code" },
129-
{ "<leader>co", "<cmd>ClaudeCodeOpen<cr>", desc = "Open Claude Terminal" },
130-
{ "<leader>cx", "<cmd>ClaudeCodeClose<cr>", desc = "Close Claude Terminal" },
127+
{ "<leader>ac", "<cmd>ClaudeCode<cr>", mode = { "n", "v" }, desc = "Toggle Claude Terminal" },
128+
{ "<leader>ak", "<cmd>ClaudeCodeSend<cr>", mode = { "n", "v" }, desc = "Send to Claude Code" },
129+
{ "<leader>ao", "<cmd>ClaudeCodeOpen<cr>", mode = { "n", "v" }, desc = "Open/Focus Claude Terminal" },
130+
{ "<leader>ax", "<cmd>ClaudeCodeClose<cr>", mode = { "n", "v" }, desc = "Close Claude Terminal" },
131131
},
132132
},
133133
}
134134
```
135135

136136
This configuration:
137137

138-
1. Uses the `dir` parameter to specify the local path to your repository
139-
2. Sets `dev = true` to enable development mode
140-
3. Sets a more verbose log level for debugging
141-
4. Adds convenient keymaps for testing
138+
1. Specifies the local repository path using the `dir` parameter.
139+
2. Enables development mode via `dev = true`.
140+
3. Sets a more verbose `log_level` for debugging.
141+
4. Includes convenient keymaps for easier testing.
142142

143143
## Configuration
144144

@@ -213,7 +213,7 @@ require("claudecode").setup({
213213
214214
- Use normal Vim window navigation (`Ctrl+w` commands)
215215
- Or toggle the terminal with `:ClaudeCode`
216-
- Open/focus with `:ClaudeCodeOpen`
216+
- Open/focus with `:ClaudeCodeOpen` (can also be used from Visual mode to switch focus after selection)
217217
- Close with `:ClaudeCodeClose`
218218
219219
4. Use Claude as normal - it will have access to your Neovim editor context!
@@ -233,12 +233,12 @@ require("claudecode").setup({
233233
No default keymaps are provided. Add your own in your configuration:
234234
235235
```lua
236-
vim.keymap.set("n", "<leader>cc", "<cmd>ClaudeCode<cr>", { desc = "Toggle Claude Terminal" })
237-
vim.keymap.set({"n", "v"}, "<leader>ck", "<cmd>ClaudeCodeSend<cr>", { desc = "Send to Claude Code" })
236+
vim.keymap.set({"n", "v"}, "<leader>ac", "<cmd>ClaudeCode<cr>", { desc = "Toggle Claude Terminal" })
237+
vim.keymap.set({"n", "v"}, "<leader>ak", "<cmd>ClaudeCodeSend<cr>", { desc = "Send to Claude Code" })
238238
239239
-- Or more specific maps:
240-
vim.keymap.set("n", "<leader>co", "<cmd>ClaudeCodeOpen<cr>", { desc = "Open Claude Terminal" })
241-
vim.keymap.set("n", "<leader>cx", "<cmd>ClaudeCodeClose<cr>", { desc = "Close Claude Terminal" })
240+
vim.keymap.set({"n", "v"}, "<leader>ao", "<cmd>ClaudeCodeOpen<cr>", { desc = "Open/Focus Claude Terminal" })
241+
vim.keymap.set({"n", "v"}, "<leader>ax", "<cmd>ClaudeCodeClose<cr>", { desc = "Close Claude Terminal" })
242242
```
243243

244244
## Architecture

lua/claudecode/terminal.lua

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -290,17 +290,18 @@ local function build_snacks_opts(effective_term_config_for_snacks, env_table)
290290
height = 0, -- 0 for full height in snacks.win
291291
relative = "editor",
292292
on_close = function(self) -- self here is the snacks.win instance
293-
if managed_snacks_terminal and managed_snacks_terminal.winid == self.winid then
293+
if managed_snacks_terminal and managed_snacks_terminal.win == self.win then
294294
managed_snacks_terminal = nil
295295
end
296296
end,
297297
},
298298
}
299299
end
300300

301-
-- Helper function to get the base claude command and necessary environment variables.
302-
-- @return string|nil The base command string.
303-
-- @return table|nil The environment variables table.
301+
--- Gets the base claude command and necessary environment variables.
302+
-- @local
303+
-- @return string|nil base_command The base command string, or nil on failure.
304+
-- @return table|nil env_table The environment variables table, or nil on failure.
304305
local function get_claude_command_and_env()
305306
local base_command = get_claude_command()
306307
if not base_command or base_command == "" then
@@ -321,6 +322,7 @@ local function get_claude_command_and_env()
321322
end
322323

323324
--- Opens or focuses the Claude terminal.
325+
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
324326
function M.open(opts_override)
325327
local provider = get_effective_terminal_provider()
326328
local effective_config = build_effective_term_config(opts_override)
@@ -382,14 +384,14 @@ function M.close()
382384
end
383385

384386
--- Toggles the Claude terminal open or closed.
387+
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
385388
function M.toggle(opts_override)
386389
local provider = get_effective_terminal_provider()
387390
local effective_config = build_effective_term_config(opts_override)
388391
local base_claude_command, claude_env_table = get_claude_command_and_env()
389392

390393
if not base_claude_command then
391-
-- Error already notified by the helper function
392-
return
394+
return -- Error already notified
393395
end
394396

395397
if provider == "snacks" then
@@ -398,15 +400,51 @@ function M.toggle(opts_override)
398400
return
399401
end
400402
local snacks_opts = build_snacks_opts(effective_config, claude_env_table)
401-
local term_instance = Snacks.terminal.toggle(base_claude_command, snacks_opts)
402-
if term_instance and term_instance:valid() then
403-
managed_snacks_terminal = term_instance
403+
404+
if managed_snacks_terminal and managed_snacks_terminal:valid() and managed_snacks_terminal.win then
405+
local claude_term_neovim_win_id = managed_snacks_terminal.win
406+
local current_neovim_win_id = vim.api.nvim_get_current_win()
407+
408+
if claude_term_neovim_win_id == current_neovim_win_id then
409+
-- Snacks.terminal.toggle will return an invalid instance or nil.
410+
-- The on_close callback (defined in build_snacks_opts) will set managed_snacks_terminal to nil.
411+
local closed_instance = Snacks.terminal.toggle(base_claude_command, snacks_opts)
412+
if closed_instance and closed_instance:valid() then
413+
-- This would be unexpected if it was supposed to close and on_close fired.
414+
-- As a fallback, ensure our state reflects what Snacks returned if it's somehow still valid.
415+
managed_snacks_terminal = closed_instance
416+
end
417+
else
418+
vim.api.nvim_set_current_win(claude_term_neovim_win_id)
419+
if managed_snacks_terminal.buf and vim.api.nvim_buf_is_valid(managed_snacks_terminal.buf) then
420+
if vim.api.nvim_buf_get_option(managed_snacks_terminal.buf, "buftype") == "terminal" then
421+
vim.api.nvim_win_call(claude_term_neovim_win_id, function()
422+
vim.cmd("startinsert")
423+
end)
424+
end
425+
end
426+
end
404427
else
405-
managed_snacks_terminal = nil -- Snacks.toggle returns nil if closed or failed
428+
local term_instance = Snacks.terminal.toggle(base_claude_command, snacks_opts)
429+
if term_instance and term_instance:valid() and term_instance.win then
430+
managed_snacks_terminal = term_instance
431+
else
432+
managed_snacks_terminal = nil -- Ensure it's nil if open failed or instance invalid
433+
if not (term_instance == nil and managed_snacks_terminal == nil) then -- Avoid notify if toggle returned nil and we set to nil
434+
vim.notify("Failed to open Snacks terminal or instance invalid after toggle.", vim.log.levels.WARN)
435+
end
436+
end
406437
end
407438
elseif provider == "native" then
408439
if is_fallback_terminal_valid() then
409-
close_fallback_terminal()
440+
local claude_term_neovim_win_id = managed_fallback_terminal_winid
441+
local current_neovim_win_id = vim.api.nvim_get_current_win()
442+
443+
if claude_term_neovim_win_id == current_neovim_win_id then
444+
close_fallback_terminal()
445+
else
446+
focus_fallback_terminal() -- This already calls startinsert
447+
end
410448
else
411449
if not open_fallback_terminal(base_claude_command, claude_env_table, effective_config) then
412450
vim.notify("Failed to open Claude terminal using native fallback (toggle).", vim.log.levels.ERROR)
@@ -415,7 +453,10 @@ function M.toggle(opts_override)
415453
end
416454
end
417455

418-
-- Test helper function to access managed terminal for testing
456+
--- Gets the managed terminal instance for testing purposes.
457+
-- NOTE: This function is intended for use in tests to inspect internal state.
458+
-- The underscore prefix indicates it's not part of the public API for regular use.
459+
-- @return table|nil The managed Snacks terminal instance, or nil.
419460
function M._get_managed_terminal_for_test()
420461
return managed_snacks_terminal
421462
end

plugin/claudecode.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ if vim.g.loaded_claudecode then
88
end
99
vim.g.loaded_claudecode = 1
1010

11-
-- If the user has set auto-load configuration, load it.
1211
-- Example in init.lua: vim.g.claudecode_auto_setup = { auto_start = true }
1312
if vim.g.claudecode_auto_setup then
1413
vim.defer_fn(function()
@@ -20,6 +19,10 @@ local terminal_ok, terminal = pcall(require, "claudecode.terminal")
2019

2120
if terminal_ok then
2221
vim.api.nvim_create_user_command("ClaudeCode", function(_opts)
22+
local current_mode = vim.fn.mode()
23+
if current_mode == "v" or current_mode == "V" or current_mode == "\22" then -- \22 is CTRL-V for blockwise visual
24+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
25+
end
2326
terminal.toggle({}) -- opts.fargs could be used for future enhancements
2427
end, {
2528
nargs = "?", -- Allow optional arguments for future enhancements

0 commit comments

Comments
 (0)