Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A lightweight Neovim plugin that embeds [ECA (Editor Code Assistant)](https://ec

1. Install via your plugin manager (see Installation below)
2. Run `:EcaChat` or press `<leader>ec`
3. Type your message and press `Ctrl+S`
3. Type your message and press `<C-s>` (or your configured submit key)


## Documentation
Expand Down
18 changes: 16 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ require("eca").setup({
chat = "<leader>ec", -- Open chat
focus = "<leader>ef", -- Focus sidebar
toggle = "<leader>et",-- Toggle sidebar

-- Chat input submit keys (per-mode). Always bound regardless of
-- `behavior.auto_set_keymaps`, since the input window is unusable without
-- a way to send. Override one or both — partial overrides preserve the
-- other mode's default.
--
-- Example "chat-app feel" override (insert-mode Enter sends, no easy
-- newline while composing):
-- submit = { insert = "<CR>" }
submit = {
normal = "<CR>",
insert = "<C-s>",
},
},

-- === WINDOWS & UI ===
Expand Down Expand Up @@ -122,9 +135,10 @@ require("eca").setup({
-- If non-empty, overrides the server-provided welcome message
message = "",

-- Tips appended under the welcome (set {} to disable)
-- Tips appended under the welcome (set {} to disable).
-- Available placeholders: {submit_key_normal}, {submit_key_insert}
tips = {
"Type your message and use CTRL+s to send",
"Type your message and press {submit_key_insert} to send",
},
},

Expand Down
13 changes: 8 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Recent updates include:
3. Open a file you want to analyze
4. Run `:EcaChat` or press `<leader>ec`
5. On first run, the server downloads automatically
6. Type your question and press `Ctrl+S` to send
6. Type your question and press `<C-s>` to send (configurable — see [Configuration](./configuration.md))

---

Expand Down Expand Up @@ -64,19 +64,22 @@ Deprecated aliases (still available but log a warning): `:EcaAddFile`, `:EcaAddS

| Shortcut | Action | Context |
|----------|--------|---------|
| `Ctrl+S` | Send message | Insert/Normal mode |
| `<CR>` | Send message (default) | Normal mode in input |
| `<C-s>` | Send message (default) | Insert mode in input |
| `Enter` | New line | Insert mode |
| `Enter` (in chat buffer) | Toggle tool call/reasoning block | Normal mode on tool call or reasoning header |
| `Esc` | Exit insert mode | Insert mode |

Submit keys are configurable via `mappings.submit.normal` and `mappings.submit.insert` — see [Configuration](./configuration.md).

---

## Using the Chat

### Sending messages
- Type in the input line starting with `> `
- Press `Enter` to insert a new line
- Press `Ctrl+S` to send
- Press `Enter` to insert a new line (insert mode)
- Press `<C-s>` (insert mode) or `<CR>` (normal mode) to send — both configurable
- Responses stream in real time with a typewriter effect (configurable)

### Interacting with responses
Expand Down Expand Up @@ -313,7 +316,7 @@ are first expanded to absolute paths on the Neovim side (including `~` expansion
```markdown
> Explain what this function does and how I can improve it
```
5. Send with `Ctrl+S`
5. Send with `<C-s>` (insert) or `<CR>` (normal)
6. Read the response and implement suggestions
7. Continue the conversation for clarifications

Expand Down
9 changes: 8 additions & 1 deletion lua/eca/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ M._defaults = {
chat = "<leader>ec",
focus = "<leader>ef",
toggle = "<leader>et",
-- Chat input submit keys (per-mode). Always bound regardless of `auto_set_keymaps`,

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

Config comment mentions auto_set_keymaps, but the actual option is behavior.auto_set_keymaps (see Config.behavior.auto_set_keymaps usage in lua/eca/init.lua). Updating the comment would avoid ambiguity for users reading the defaults.

Suggested change
-- Chat input submit keys (per-mode). Always bound regardless of `auto_set_keymaps`,
-- Chat input submit keys (per-mode). Always bound regardless of `behavior.auto_set_keymaps`,

Copilot uses AI. Check for mistakes.
-- since the input window is unusable without a way to send.
submit = {
normal = "<CR>",
insert = "<C-s>",
},
},
windows = {
wrap = true,
Expand Down Expand Up @@ -62,7 +68,8 @@ M._defaults = {
welcome = {
message = "", -- If non-empty, overrides server-provided welcome message
tips = {
"Type your message and use CTRL+s to send", -- Tips appended under the welcome (set empty list {} to disable)
-- Available placeholders: {submit_key_normal}, {submit_key_insert}
"Type your message and press {submit_key_insert} to send", -- Tips appended under the welcome (set empty list {} to disable)
},
},
typing = {
Expand Down
16 changes: 12 additions & 4 deletions lua/eca/sidebar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -617,12 +617,12 @@ end
---@private
---@param container NuiSplit
function M:_setup_input_keymaps(container)
-- Setup keymaps for input container
container:map("n", "<C-s>", function()
local submit = Config.mappings.submit
container:map("n", submit.normal, function()
self:_handle_input()
end, { noremap = true, silent = true })

container:map("i", "<C-s>", function()
container:map("i", submit.insert, function()
self:_handle_input()
end, { noremap = true, silent = true })
end
Expand Down Expand Up @@ -1163,8 +1163,16 @@ function M:_update_welcome_content()
local tips = cfg.tips or {}

if #tips > 0 then
local submit = Config.mappings.submit
local placeholders = {
submit_key_normal = submit.normal,
submit_key_insert = submit.insert,
}
for _, tip in ipairs(tips) do
table.insert(lines, tip)
local rendered = tip:gsub("{(.-)}", function(k)
return placeholders[k] or ("{" .. k .. "}")
end)
table.insert(lines, rendered)
end
end

Expand Down
175 changes: 175 additions & 0 deletions tests/test_config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -609,4 +609,179 @@ T["behavior_validation"]["typing.enabled=true enables gradual display"] = functi
eq(child.lua_get("_G.tick_delay"), 5)
end

-- ===== Mappings tests =====

T["mappings"] = MiniTest.new_set()

T["mappings"]["submit defaults bind <CR> in normal and <C-s> in insert"] = function()
child.lua([[
local Server = require('eca.server').new()
local State = require('eca.state').new()
local Mediator = require('eca.mediator').new(Server, State)
local Sidebar = require('eca.sidebar')
local sidebar = Sidebar.new(1, Mediator)

sidebar:open()

local input_bufnr = sidebar.containers.input.bufnr

-- Normalize a key string the same way nvim does internally for keymaps
local function norm(k)
return vim.api.nvim_replace_termcodes(k, true, true, true)
end
local function has_mapping(mode, key)
local target = norm(key)
for _, m in ipairs(vim.api.nvim_buf_get_keymap(input_bufnr, mode)) do
if norm(m.lhs) == target then
return true
end
end
return false
end

_G.has_normal_cr = has_mapping("n", "<CR>")
_G.has_insert_cs = has_mapping("i", "<C-s>")
]])

eq(child.lua_get("_G.has_normal_cr"), true)
eq(child.lua_get("_G.has_insert_cs"), true)
end

T["mappings"]["submit partial override preserves other mode default"] = function()
child.lua([[
local Config = require('eca.config')
Config.override({
mappings = {
submit = {
insert = "<C-x>",
},
},
})

local Server = require('eca.server').new()
local State = require('eca.state').new()
local Mediator = require('eca.mediator').new(Server, State)
local Sidebar = require('eca.sidebar')
local sidebar = Sidebar.new(1, Mediator)

sidebar:open()

local input_bufnr = sidebar.containers.input.bufnr

local function norm(k)
return vim.api.nvim_replace_termcodes(k, true, true, true)
end
local function has_mapping(mode, key)
local target = norm(key)
for _, m in ipairs(vim.api.nvim_buf_get_keymap(input_bufnr, mode)) do
if norm(m.lhs) == target then
return true
end
end
return false
end

_G.has_normal_cr = has_mapping("n", "<CR>") -- default preserved
_G.has_insert_cx = has_mapping("i", "<C-x>") -- override applied
_G.has_insert_cs = has_mapping("i", "<C-s>") -- old default unbound
]])

eq(child.lua_get("_G.has_normal_cr"), true)
eq(child.lua_get("_G.has_insert_cx"), true)
eq(child.lua_get("_G.has_insert_cs"), false)
end

T["mappings"]["submit full override binds both modes"] = function()
child.lua([[
local Config = require('eca.config')
Config.override({
mappings = {
submit = {
normal = "<C-y>",
insert = "<C-x>",
},
},
})

local Server = require('eca.server').new()
local State = require('eca.state').new()
local Mediator = require('eca.mediator').new(Server, State)
local Sidebar = require('eca.sidebar')
local sidebar = Sidebar.new(1, Mediator)

sidebar:open()

local input_bufnr = sidebar.containers.input.bufnr

local function norm(k)
return vim.api.nvim_replace_termcodes(k, true, true, true)
end
local function has_mapping(mode, key)
local target = norm(key)
for _, m in ipairs(vim.api.nvim_buf_get_keymap(input_bufnr, mode)) do
if norm(m.lhs) == target then
return true
end
end
return false
end

_G.has_normal_cy = has_mapping("n", "<C-y>")
_G.has_insert_cx = has_mapping("i", "<C-x>")
_G.has_normal_cr = has_mapping("n", "<CR>")
]])

eq(child.lua_get("_G.has_normal_cy"), true)
eq(child.lua_get("_G.has_insert_cx"), true)
eq(child.lua_get("_G.has_normal_cr"), false)
end

T["mappings"]["welcome tip substitutes submit key placeholders"] = function()
child.lua([[
local Config = require('eca.config')
Config.override({
mappings = {
submit = {
normal = "<C-y>",
insert = "<C-x>",
},
},
windows = {
chat = {
welcome = {
tips = {
"normal={submit_key_normal} insert={submit_key_insert} unknown={nope}",
},
},
},
},
})

local Server = require('eca.server').new()
local State = require('eca.state').new()
local Mediator = require('eca.mediator').new(Server, State)
-- Force a non-empty welcome_message so the tips branch runs
function Mediator:welcome_message() return "welcome" end

local Sidebar = require('eca.sidebar')
local sidebar = Sidebar.new(1, Mediator)
sidebar:open()
sidebar:_update_welcome_content()

local chat_bufnr = sidebar.containers.chat.bufnr
local lines = vim.api.nvim_buf_get_lines(chat_bufnr, 0, -1, false)
_G.lines = lines
]])

local lines = child.lua_get("_G.lines")
local found = false
for _, line in ipairs(lines) do
if line == "normal=<C-y> insert=<C-x> unknown={nope}" then
found = true
break
end
end
eq(found, true)
end

return T
Loading