Skip to content
Merged
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
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,16 @@ require('opencode').setup({
},
input_window = {
['<S-cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' } }, -- Submit prompt (normal mode and insert mode)
['<esc>'] = { 'close' }, -- Close UI windows
['<C-c>'] = { 'cancel' }, -- Cancel opencode request while it is running
['<esc>'] = { 'close', defer_to_completion = true }, -- Close UI windows
['<C-c>'] = { 'cancel', defer_to_completion = true }, -- Cancel opencode request while it is running
['~'] = { 'mention_file', mode = 'i' }, -- Pick a file and add to context. See File Mentions section
['@'] = { 'mention', mode = 'i' }, -- Insert mention (file/agent)
['/'] = { 'slash_commands', mode = 'i' }, -- Pick a command to run in the input window
['#'] = { 'context_items', mode = 'i' }, -- Manage context items (current file, selection, diagnostics, mentioned files)
['<M-v>'] = { 'paste_image', mode = 'i' }, -- Paste image from clipboard as attachment
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' } }, -- Toggle between input and output panes
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' } }, -- Navigate to previous prompt in history
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' } }, -- Navigate to next prompt in history
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' }, defer_to_completion = true }, -- Toggle between input and output panes
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' }, defer_to_completion = true }, -- Navigate to previous prompt in history
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' }, defer_to_completion = true }, -- Navigate to next prompt in history
['<M-m>'] = { 'switch_mode' }, -- Switch between modes (build/plan)
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' } }, -- Cycle through available model variants
},
Expand Down Expand Up @@ -368,6 +368,7 @@ Each keymap entry is a table consising of:
- Or a custom function: `{ function() ... end }`
- An optional mode: `{ 'toggle', mode = { 'n', 'i' } }`
- An optional desc: `{'toggle', desc = 'Toggle Opencode' }`
- An optional defer_to_completion: `{'toggle', defer_to_completion = true }` if true, when completion menu is open, it will defer to the completion keymaps instead of triggering the action

#### Disabling Specific Keymaps

Expand Down Expand Up @@ -661,13 +662,14 @@ Example keymap for silent add:

**add_visual_selection_inline** inserts the visually selected code directly into the input buffer as a Markdown code block, prefixed with the file path:

```
````
**`path/to/file.lua`**

```lua
<selected text>
```
```
````

````
Comment on lines +670 to +672

The cursor is left in normal mode in the input buffer so you can type your prompt around the inserted snippet.

Expand All @@ -692,7 +694,7 @@ Run a prompt in a new session using the Plan agent and disabling current file co
```vim
:Opencode run new_session "Please help me plan a new feature" agent=plan context.current_file.enabled=false
:Opencode run "Fix the bug in the current file" model=github-copilot/claude-sonnet-4
```
````

## 👮 Permissions

Expand Down
40 changes: 20 additions & 20 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,26 @@ M.defaults = {
['<leader>ods'] = { 'debug_session', desc = 'Open raw session debug view' },
},
input_window = {
['<cr>'] = { 'submit_input_prompt', mode = { 'n' }, desc = 'Submit prompt' },
['<S-cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' }, desc = 'Submit prompt' },
['<esc>'] = { 'close', desc = 'Close Opencode windows' },
['<C-c>'] = { 'cancel', desc = 'Cancel running request' },
['~'] = { 'mention_file', mode = 'i', desc = 'Mention file in context' },
['@'] = { 'mention', mode = 'i', desc = 'Open mention picker' },
['/'] = { 'slash_commands', mode = 'i', desc = 'Open slash commands picker' },
['#'] = { 'context_items', mode = 'i', desc = 'Open context items picker' },
['<M-v>'] = { 'paste_image', mode = 'i', desc = 'Paste image from clipboard' },
['<tab>'] = { 'toggle_pane', mode = { 'n' }, desc = 'Toggle input/output panes' },
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' }, desc = 'Previous prompt history item' },
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' }, desc = 'Next prompt history item' },
['<M-m>'] = { 'switch_mode', mode = { 'n', 'i' }, desc = 'Switch agent mode' },
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' }, desc = 'Cycle model variants' },
['<M-i>'] = { 'toggle_input', mode = { 'n', 'i' }, desc = 'Toggle input window' },
['gr'] = { 'references', desc = 'Browse code references' },
['<leader>oS'] = { 'select_child_session', desc = 'Select child session' },
['<leader>oD'] = { 'debug_message', desc = 'Open raw message debug view' },
['<leader>oO'] = { 'debug_output', desc = 'Open raw output debug view' },
['<leader>ods'] = { 'debug_session', desc = 'Open raw session debug view' },
['<cr>'] = { 'submit_input_prompt', mode = { 'n' }, desc = 'Submit prompt' },
['<S-cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' }, desc = 'Submit prompt' },
['<esc>'] = { 'close', desc = 'Close Opencode windows', defer_to_completion = true },
['<C-c>'] = { 'cancel', desc = 'Cancel running request' , defer_to_completion = true },
['~'] = { 'mention_file', mode = 'i', desc = 'Mention file in context' },
['@'] = { 'mention', mode = 'i', desc = 'Open mention picker' },
['/'] = { 'slash_commands', mode = 'i', desc = 'Open slash commands picker' },
['#'] = { 'context_items', mode = 'i', desc = 'Open context items picker' },
['<M-v>'] = { 'paste_image', mode = 'i', desc = 'Paste image from clipboard' },
['<tab>'] = { 'toggle_pane', mode = { 'n' }, desc = 'Toggle input/output panes', defer_to_completion = true },
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' }, desc = 'Previous prompt history item', defer_to_completion = true },
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' }, desc = 'Next prompt history item' , defer_to_completion = true },
['<M-m>'] = { 'switch_mode', mode = { 'n', 'i' }, desc = 'Switch agent mode' },
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' }, desc = 'Cycle model variants' },
['<M-i>'] = { 'toggle_input', mode = { 'n', 'i' }, desc = 'Toggle input window' },
['gr'] = { 'references', desc = 'Browse code references' },
['<leader>oS'] = { 'select_child_session', desc = 'Select child session' },
['<leader>oD'] = { 'debug_message', desc = 'Open raw message debug view' },
['<leader>oO'] = { 'debug_output', desc = 'Open raw output debug view' },
['<leader>ods'] = { 'debug_session', desc = 'Open raw session debug view' },
},
session_picker = {
rename_session = { '<C-r>', desc = 'Rename selected session' },
Expand Down
15 changes: 8 additions & 7 deletions lua/opencode/keymap.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
local config = require('opencode.config')
local M = {}

local function is_completion_visible()
return require('opencode.ui.completion').is_completion_visible()
end

---@param key_binding string The key binding to feed if completion is visible
---@param callback function The callback to execute if completion is not visible
local function wrap_with_completion_check(key_binding, callback)
return function()
if is_completion_visible() then
Expand All @@ -13,11 +16,10 @@ local function wrap_with_completion_check(key_binding, callback)
end
end

---@param keymap_config table The keymap configuration table
---@param keymap_config table<string, OpencodeKeymapEntry> The keymap configuration table
---@param default_modes table Default modes for these keymaps
---@param base_opts table Base options to use for all keymaps
---@param defer_to_completion boolean? Whether to defer to completion engine when visible
local function process_keymap_entry(keymap_config, default_modes, base_opts, defer_to_completion)
local function process_keymap_entry(keymap_config, default_modes, base_opts)
local api = require('opencode.api')
local cmds = api.commands

Expand All @@ -41,7 +43,7 @@ local function process_keymap_entry(keymap_config, default_modes, base_opts, def
opts.desc = config_entry.desc or cmds[func_name] and cmds[func_name].desc

if callback then
if defer_to_completion then
if config_entry.defer_to_completion then
callback = wrap_with_completion_check(key_binding, callback)
end
vim.keymap.set(modes, key_binding, callback, opts)
Expand All @@ -61,13 +63,12 @@ end

---@param keymap_config table Window keymap configuration
---@param buf_id integer Buffer ID to set keymaps for
---@param defer_to_completion boolean? Whether to defer to completion engine when visible (default: false)
function M.setup_window_keymaps(keymap_config, buf_id, defer_to_completion)
function M.setup_window_keymaps(keymap_config, buf_id)
if not vim.api.nvim_buf_is_valid(buf_id) then
return
end

process_keymap_entry(keymap_config or {}, { 'n' }, { silent = true, buffer = buf_id }, defer_to_completion)
process_keymap_entry(keymap_config or {}, { 'n' }, { silent = true, buffer = buf_id })
end

return M
1 change: 1 addition & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
---@field [1] string # Function name
---@field mode? string|string[] # Mode(s) for the keymap
---@field desc? string # Keymap description
---@field defer_to_completion? boolean # Whether to defer the keymap when completion menu is open

---@class OpencodeKeymapEditor : table<string, OpencodeKeymapEntry>
---@class OpencodeKeymapInputWindow : table<string, OpencodeKeymapEntry>
Expand Down
6 changes: 1 addition & 5 deletions lua/opencode/ui/input_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,6 @@ function M.handle_submit()
return false
end
---@cast windows { input_buf: integer }
local completion = require('opencode.ui.completion')
if completion.is_completion_visible() then
return false
end

local input_content = table.concat(vim.api.nvim_buf_get_lines(windows.input_buf, 0, -1, false), '\n')
vim.api.nvim_buf_set_lines(windows.input_buf, 0, -1, false, {})
Expand Down Expand Up @@ -473,7 +469,7 @@ function M.setup_keymaps(windows)
keymaps_set_for_buf[windows.input_buf] = true

local keymap = require('opencode.keymap')
keymap.setup_window_keymaps(config.keymap.input_window, windows.input_buf, true)
keymap.setup_window_keymaps(config.keymap.input_window, windows.input_buf)
end

function M.setup_autocmds(windows, group)
Expand Down
105 changes: 105 additions & 0 deletions tests/unit/keymap_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ describe('opencode.keymap', function()
local mock_api
local keymap

-- Mock completion module state (controlled per test)
local mock_completion
local original_nvim_feedkeys
local feedkeys_calls = {}

before_each(function()
set_keymaps = {}
cmd_calls = {}
feedkeys_calls = {}
original_keymap_set = vim.keymap.set
original_vim_cmd = vim.cmd
original_nvim_feedkeys = vim.api.nvim_feedkeys

-- Mock the functions to capture calls
vim.keymap.set = function(modes, key, callback, opts)
Expand All @@ -35,6 +42,10 @@ describe('opencode.keymap', function()
table.insert(cmd_calls, command)
end

vim.api.nvim_feedkeys = function(keys, mode, escape_ks)
table.insert(feedkeys_calls, { keys = keys, mode = mode, escape_ks = escape_ks })
end

-- Mock the API module before requiring keymap
mock_api = {
open_input = function() end,
Expand All @@ -61,6 +72,14 @@ describe('opencode.keymap', function()
local mock_config = {}
package.loaded['opencode.config'] = mock_config

-- Mock the completion module (visible = false by default)
mock_completion = {
is_completion_visible = function()
return false
end,
}
package.loaded['opencode.ui.completion'] = mock_completion

-- Now require the keymap module
keymap = require('opencode.keymap')
end)
Expand All @@ -69,12 +88,14 @@ describe('opencode.keymap', function()
-- Restore original functions
vim.keymap.set = original_keymap_set
vim.cmd = original_vim_cmd
vim.api.nvim_feedkeys = original_nvim_feedkeys

-- Clean up package loading
package.loaded['opencode.keymap'] = nil
package.loaded['opencode.api'] = nil
package.loaded['opencode.state'] = nil
package.loaded['opencode.config'] = nil
package.loaded['opencode.ui.completion'] = nil
end)

describe('normalize_keymap', function()
Expand Down Expand Up @@ -226,4 +247,88 @@ describe('opencode.keymap', function()
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
end)

describe('defer_to_completion', function()
it('calls the callback directly when completion is not visible', function()
local callback_called = false
local test_keymap = {
editor = {
['<tab>'] = {
function()
callback_called = true
end,
defer_to_completion = true,
desc = 'Tab with completion defer',
},
},
}

keymap.setup(test_keymap)

assert.equal(1, #set_keymaps, 'Should register one keymap')
local registered = set_keymaps[1]

registered.callback()

assert.is_true(callback_called, 'Callback should be called when completion is not visible')
assert.equal(0, #feedkeys_calls, 'Should not feed keys when completion is not visible')
end)

it('feeds the key binding when completion is visible instead of calling the callback', function()
mock_completion.is_completion_visible = function()
return true
end

local callback_called = false
local test_keymap = {
editor = {
['<tab>'] = {
function()
callback_called = true
end,
defer_to_completion = true,
desc = 'Tab with completion defer',
},
},
}

keymap.setup(test_keymap)

assert.equal(1, #set_keymaps, 'Should register one keymap')
local registered = set_keymaps[1]

registered.callback()

assert.is_false(callback_called, 'Callback should NOT be called when completion is visible')
assert.equal(1, #feedkeys_calls, 'Should feed the key binding to the completion engine')
end)

it('does not wrap with completion check when defer_to_completion is not set', function()
local callback_called = false
local test_keymap = {
editor = {
['<tab>'] = {
function()
callback_called = true
end,
desc = 'Tab without defer',
},
},
}

mock_completion.is_completion_visible = function()
return true
end

keymap.setup(test_keymap)

assert.equal(1, #set_keymaps, 'Should register one keymap')
local registered = set_keymaps[1]

registered.callback()

assert.is_true(callback_called, 'Callback should be called directly without completion check')
assert.equal(0, #feedkeys_calls, 'Should not feed keys when defer_to_completion is not set')
end)
end)
end)
Loading