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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ require('opencode').setup({
['<leader>oR'] = { 'rename_session' }, -- Rename current session
['<leader>op'] = { 'configure_provider' }, -- Quick provider and model switch from predefined list
['<leader>oV'] = { 'configure_variant' }, -- Switch model variant for the current model
['<leader>oy'] = { 'add_visual_selection', mode = {'v'} },
['<leader>oz'] = { 'toggle_zoom' }, -- Zoom in/out on the Opencode windows
['<leader>ov'] = { 'paste_image'}, -- Paste image from clipboard into current session
['<leader>od'] = { 'diff_open' }, -- Opens a diff tab of a modified file since the last opencode prompt
Expand Down Expand Up @@ -612,6 +613,7 @@ The plugin provides the following actions that can be triggered via keymaps, com
| Toggle tools output (diffs, cmd output, etc.) | `<leader>ott` | `:Opencode toggle_tool_output` | `require('opencode.api').toggle_tool_output()` |
| Toggle reasoning output (thinking steps) | `<leader>otr` | `:Opencode toggle_reasoning_output` | `require('opencode.api').toggle_reasoning_output()` |
| Open a quick chat input with selection/current line context | `<leader>o/` | `:Opencode quick_chat` | `require('opencode.api').quick_chat()` |
| Add visual selection to context | `<leader>oy` | `:Opencode add_visual_selection` | `require('opencode.api').add_visual_selection()` |

### Run opts

Expand Down
21 changes: 21 additions & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,21 @@ M.review = Promise.async(function(args)
end)
end)

--- Add the current visual selection to the context without opening/focusing the panel.
--- Can be called from any buffer. Selections accumulate across files.
M.add_visual_selection = Promise.async(
---@param _ any Unused
---@param range OpencodeSelectionRange
function(_, range)
local context = require('opencode.context')
local added = context.add_visual_selection(range)

if added then
M.open_input():await()
end
end
)

---@type table<string, OpencodeUICommand>
M.commands = {
open = {
Expand Down Expand Up @@ -1369,6 +1384,11 @@ M.commands = {
desc = 'Browse code references from conversation',
fn = M.references,
},

add_visual_selection = {
desc = 'Add current visual selection to context',
fn = M.add_visual_selection,
},
}

M.slash_commands_map = {
Expand Down Expand Up @@ -1449,6 +1469,7 @@ M.legacy_command_map = {

function M.route_command(opts)
local args = vim.split(opts.args or '', '%s+', { trimempty = true })
---@type OpencodeSelectionRange|nil
local range = nil

if opts.range and opts.range > 0 then
Expand Down
1 change: 1 addition & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ M.defaults = {
['<leader>oR'] = { 'rename_session', desc = 'Rename session' },
['<leader>op'] = { 'configure_provider', desc = 'Configure provider' },
['<leader>oV'] = { 'configure_variant', desc = 'Configure model variant' },
['<leader>oy'] = { 'add_visual_selection', desc = 'Add visual selection to context', mode = { 'v' } },
['<leader>oz'] = { 'toggle_zoom', desc = 'Toggle zoom' },
['<leader>ov'] = { 'paste_image', desc = 'Paste image from clipboard' },
['<leader>od'] = { 'diff_open', desc = 'Open diff view' },
Expand Down
36 changes: 36 additions & 0 deletions lua/opencode/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ function M.clear_selections()
ChatContext.clear_selections()
end

--- Captures the current visual selection and adds it to the context.
--- This can be called from any buffer at any time, even when the panel is already open.
--- Selections persist across file switches and accumulate across buffers.
---@param range? OpencodeSelectionRange
---@return boolean success Whether a selection was successfully added
function M.add_visual_selection(range)
local buf = vim.api.nvim_get_current_buf()

if not util.is_buf_a_file(buf) then
vim.notify('Cannot add selection: not a file buffer', vim.log.levels.WARN)
return false
end

local current_selection = BaseContext.get_current_selection(nil, range)
if not current_selection then
vim.notify('No visual selection found', vim.log.levels.WARN)
return false
end

local file = BaseContext.get_current_file_for_selection(buf)
if not file then
vim.notify('Cannot determine file for selection', vim.log.levels.WARN)
return false
end

local selection = BaseContext.new_selection(file, current_selection.text, current_selection.lines)
M.add_selection(selection)

vim.notify(
string.format('Selection added from %s (lines %s)', file.name, current_selection.lines),
vim.log.levels.INFO
)

return true
end

function M.add_file(file)
local is_file = vim.fn.filereadable(file) == 1
local is_dir = vim.fn.isdirectory(file) == 1
Expand Down
24 changes: 23 additions & 1 deletion lua/opencode/context/base_context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,34 @@ function M.get_current_cursor_data(buf, win, context_config)
end

---@param context_config? OpencodeContextConfig
---@param range? OpencodeSelectionRange
---@return table|nil
function M.get_current_selection(context_config)
function M.get_current_selection(context_config, range)
if not M.is_context_enabled('selection', context_config) then
return nil
end

if range and range.start and range.stop then
local buf = vim.api.nvim_get_current_buf()
local start_line = math.floor(range.start)
local end_line = math.floor(range.stop)
local lines = vim.api.nvim_buf_get_lines(buf, start_line - 1, end_line, false)
local text = table.concat(lines, '\n')

if not text or text == '' then
return nil
end

if vim.fn.mode() == 'V' then
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'nx', true)
end

return {
text = text:match('[^%s]') and text or nil,
lines = start_line .. ', ' .. end_line,
}
end

-- Return nil if not in a visual mode
if not vim.fn.mode():match('[vV\022]') then
return nil
Expand Down
9 changes: 2 additions & 7 deletions lua/opencode/context/chat_context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,9 @@ function M.load()
local current_file = base_context.get_current_file(buf)
local cursor_data = base_context.get_current_cursor_data(buf, win)

local should_update_file, is_different_file = M.should_update_current_file(current_file)
local should_update_file = M.should_update_current_file(current_file)

if should_update_file then
if is_different_file then
M.context.selections = {}
end

M.context.current_file = current_file
if M.context.current_file then
M.context.current_file.sent_at = nil
Expand All @@ -399,8 +395,7 @@ function M.load()
if current_selection then
local selection_file = base_context.get_current_file_for_selection(buf)
if selection_file then
local selection =
base_context.new_selection(selection_file, current_selection.text, current_selection.lines)
local selection = base_context.new_selection(selection_file, current_selection.text, current_selection.lines)
M.add_selection(selection)
end
end
Expand Down
4 changes: 4 additions & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,7 @@
---@field messages number Number of messages reverted
---@field tool_calls number Number of tool calls reverted
---@field files table<string, {additions: number, deletions: number}> Summary of file changes reverted

---@class OpencodeSelectionRange
---@field start number Starting line number (inclusive)
---@field stop number Ending line number (inclusive)
Loading