Skip to content

Commit 9c58098

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 4cf3be2 + bb31b54 commit 9c58098

8 files changed

Lines changed: 288 additions & 2 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ require('opencode').setup({
148148
['<leader>op'] = { 'configure_provider' }, -- Quick provider and model switch from predefined list
149149
['<leader>oV'] = { 'configure_variant' }, -- Switch model variant for the current model
150150
['<leader>oy'] = { 'add_visual_selection', mode = {'v'} },
151+
['<leader>oY'] = { 'add_visual_selection_inline', mode = {'v'} }, -- Insert visual selection as inline code block in the input buffer
151152
['<leader>oz'] = { 'toggle_zoom' }, -- Zoom in/out on the Opencode windows
152153
['<leader>ov'] = { 'paste_image'}, -- Paste image from clipboard into current session
153154
['<leader>od'] = { 'diff_open' }, -- Opens a diff tab of a modified file since the last opencode prompt
@@ -232,6 +233,7 @@ require('opencode').setup({
232233
use_vim_ui_select = false, -- If true, render questions/prompts with vim.ui.select instead of showing them inline in the output buffer.
233234
},
234235
output = {
236+
filetype = 'opencode_output', -- Filetype assigned to the output buffer (default: 'opencode_output')
235237
tools = {
236238
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
237239
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
@@ -645,6 +647,7 @@ The plugin provides the following actions that can be triggered via keymaps, com
645647
| Toggle reasoning output (thinking steps) | `<leader>otr` | `:Opencode toggle_reasoning_output` | `require('opencode.api').toggle_reasoning_output()` |
646648
| Open a quick chat input with selection/current line context | `<leader>o/` | `:Opencode quick_chat` | `require('opencode.api').quick_chat()` |
647649
| Add visual selection to context | `<leader>oy` | `:Opencode add_visual_selection` | `require('opencode.api').add_visual_selection(opts?)` |
650+
| Insert visual selection inline into input | `<leader>oY` | `:Opencode add_visual_selection_inline` | `require('opencode.api').add_visual_selection_inline(opts?)` |
648651

649652
**add_visual_selection opts:**
650653

@@ -653,9 +656,21 @@ The plugin provides the following actions that can be triggered via keymaps, com
653656
Example keymap for silent add:
654657

655658
```lua
656-
['<leader>oY'] = { 'add_visual_selection', { open_input = false }, mode = {'v'} }
659+
['<leader>oy'] = { 'add_visual_selection', { open_input = false }, mode = {'v'} }
657660
```
658661

662+
**add_visual_selection_inline** inserts the visually selected code directly into the input buffer as a Markdown code block, prefixed with the file path:
663+
664+
```
665+
**`path/to/file.lua`**
666+
667+
```lua
668+
<selected text>
669+
```
670+
```
671+
672+
The cursor is left in normal mode in the input buffer so you can type your prompt around the inserted snippet.
673+
659674
### Run opts
660675
661676
You can pass additional options when running a prompt via command or API:

lua/opencode/api.lua

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,29 @@ M.add_visual_selection = Promise.async(
10851085
end
10861086
)
10871087

1088+
M.add_visual_selection_inline = Promise.async(
1089+
---@param opts? {open_input?: boolean}
1090+
---@param range OpencodeSelectionRange
1091+
function(opts, range)
1092+
opts = vim.tbl_extend('force', { open_input = true }, opts or {})
1093+
local context = require('opencode.context')
1094+
local text = context.build_inline_selection_text(range)
1095+
1096+
if not text then
1097+
return
1098+
end
1099+
1100+
M.open_input():await()
1101+
local input_window = require('opencode.ui.input_window')
1102+
input_window._append_to_input(text)
1103+
vim.schedule(function()
1104+
if vim.fn.mode() ~= 'n' then
1105+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'n', false)
1106+
end
1107+
end)
1108+
end
1109+
)
1110+
10881111
---@type table<string, OpencodeUICommand>
10891112
M.commands = {
10901113
open = {
@@ -1453,6 +1476,11 @@ M.commands = {
14531476
desc = 'Add current visual selection to context',
14541477
fn = M.add_visual_selection,
14551478
},
1479+
1480+
add_visual_selection_inline = {
1481+
desc = 'Insert visual selection as inline code block in the input buffer',
1482+
fn = M.add_visual_selection_inline,
1483+
},
14561484
}
14571485

14581486
M.slash_commands_map = {

lua/opencode/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ M.defaults = {
4040
['<leader>op'] = { 'configure_provider', desc = 'Configure provider' },
4141
['<leader>oV'] = { 'configure_variant', desc = 'Configure model variant' },
4242
['<leader>oy'] = { 'add_visual_selection', mode = { 'v' }, desc = 'Add visual selection to context' },
43+
['<leader>oY'] = { 'add_visual_selection_inline', mode = { 'v' }, desc = 'Insert visual selection inline into input' },
4344
['<leader>oz'] = { 'toggle_zoom', desc = 'Toggle zoom' },
4445
['<leader>ov'] = { 'paste_image', desc = 'Paste image from clipboard' },
4546
['<leader>od'] = { 'diff_open', desc = 'Open diff view' },
@@ -137,6 +138,7 @@ M.defaults = {
137138
frames = { '', '', '', '', '', '', '', '', '', '' },
138139
},
139140
output = {
141+
filetype = 'opencode_output',
140142
rendering = {
141143
markdown_debounce_ms = 250,
142144
on_data_rendered = nil,

lua/opencode/context.lua

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,48 @@ function M.add_visual_selection(range)
165165
return true
166166
end
167167

168+
--- Captures the current visual selection and returns the text to be inserted inline
169+
--- into the opencode input buffer, in the form:
170+
---
171+
--- **`path/to/file`**
172+
---
173+
--- ```<filetype>
174+
--- <selected text>
175+
--- ```
176+
---
177+
---@param range? OpencodeSelectionRange
178+
---@return string|nil text The formatted text to insert, or nil on failure
179+
function M.build_inline_selection_text(range)
180+
local buf = vim.api.nvim_get_current_buf()
181+
182+
if not util.is_buf_a_file(buf) then
183+
vim.notify('Cannot add selection: not a file buffer', vim.log.levels.WARN)
184+
return nil
185+
end
186+
187+
local current_selection = BaseContext.get_current_selection(nil, range)
188+
if not current_selection then
189+
vim.notify('No visual selection found', vim.log.levels.WARN)
190+
return nil
191+
end
192+
193+
local file = BaseContext.get_current_file_for_selection(buf)
194+
if not file then
195+
vim.notify('Cannot determine file for selection', vim.log.levels.WARN)
196+
return nil
197+
end
198+
199+
local filetype = vim.bo[buf].filetype or ''
200+
local text = string.format(
201+
'**`%s`**\n\n```%s\n%s\n```',
202+
file.path,
203+
filetype,
204+
current_selection.text
205+
)
206+
207+
return text
208+
end
209+
168210
function M.add_file(file)
169211
local is_file = vim.fn.filereadable(file) == 1
170212
local is_dir = vim.fn.isdirectory(file) == 1

lua/opencode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
---@field tools { show_output: boolean, show_reasoning_output: boolean }
179179
---@field rendering OpencodeUIOutputRenderingConfig
180180
---@field always_scroll_to_bottom boolean
181+
---@field filetype string
181182

182183
---@class OpencodeUIPickerConfig
183184
---@field snacks_layout? snacks.picker.layout.Config

lua/opencode/ui/output_window.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ M.namespace = vim.api.nvim_create_namespace('opencode_output')
66

77
function M.create_buf()
88
local output_buf = vim.api.nvim_create_buf(false, true)
9-
vim.api.nvim_set_option_value('filetype', 'opencode_output', { buf = output_buf })
9+
local filetype = config.ui.output.filetype or 'opencode_output'
10+
vim.api.nvim_set_option_value('filetype', filetype, { buf = output_buf })
1011

1112
local buffixwin = require('opencode.ui.buf_fix_win')
1213
buffixwin.fix_to_win(output_buf, function()

tests/unit/context_spec.lua

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,3 +1140,158 @@ describe('add_visual_selection API', function()
11401140
vim.notify = original_notify
11411141
end)
11421142
end)
1143+
1144+
describe('build_inline_selection_text', function()
1145+
local context
1146+
local BaseContext
1147+
local util
1148+
1149+
before_each(function()
1150+
context = require('opencode.context')
1151+
BaseContext = require('opencode.context.base_context')
1152+
util = require('opencode.util')
1153+
end)
1154+
1155+
it('should return formatted inline text for a visual selection', function()
1156+
local original_is_buf_a_file = util.is_buf_a_file
1157+
local original_get_current_selection = BaseContext.get_current_selection
1158+
local original_get_current_file_for_selection = BaseContext.get_current_file_for_selection
1159+
local original_get_current_buf = vim.api.nvim_get_current_buf
1160+
1161+
util.is_buf_a_file = function()
1162+
return true
1163+
end
1164+
BaseContext.get_current_selection = function()
1165+
return { text = 'function foo()\n return 42\nend', lines = '10, 12' }
1166+
end
1167+
BaseContext.get_current_file_for_selection = function()
1168+
return { path = '/tmp/test.lua', name = 'test.lua', extension = 'lua' }
1169+
end
1170+
vim.api.nvim_get_current_buf = function()
1171+
return 5
1172+
end
1173+
1174+
-- Mock vim.bo to return a filetype
1175+
local original_bo = vim.bo
1176+
vim.bo = setmetatable({}, {
1177+
__index = function(_, buf)
1178+
return { filetype = 'lua' }
1179+
end,
1180+
})
1181+
1182+
local text = context.build_inline_selection_text()
1183+
1184+
assert.is_not_nil(text)
1185+
assert.is_not_nil(text:match('%*%*`/tmp/test%.lua`%*%*'))
1186+
assert.is_not_nil(text:match('```lua'))
1187+
assert.is_not_nil(text:match('function foo%(%)'))
1188+
assert.is_not_nil(text:match('```$'))
1189+
1190+
util.is_buf_a_file = original_is_buf_a_file
1191+
BaseContext.get_current_selection = original_get_current_selection
1192+
BaseContext.get_current_file_for_selection = original_get_current_file_for_selection
1193+
vim.api.nvim_get_current_buf = original_get_current_buf
1194+
vim.bo = original_bo
1195+
end)
1196+
1197+
it('should return nil and notify when not a file buffer', function()
1198+
local original_is_buf_a_file = util.is_buf_a_file
1199+
local original_get_current_buf = vim.api.nvim_get_current_buf
1200+
1201+
util.is_buf_a_file = function()
1202+
return false
1203+
end
1204+
vim.api.nvim_get_current_buf = function()
1205+
return 10
1206+
end
1207+
1208+
local original_notify = vim.notify
1209+
local notifications = {}
1210+
vim.notify = function(msg, level)
1211+
table.insert(notifications, { msg = msg, level = level })
1212+
end
1213+
1214+
local text = context.build_inline_selection_text()
1215+
1216+
assert.is_nil(text)
1217+
assert.equal(1, #notifications)
1218+
assert.equal('Cannot add selection: not a file buffer', notifications[1].msg)
1219+
assert.equal(vim.log.levels.WARN, notifications[1].level)
1220+
1221+
util.is_buf_a_file = original_is_buf_a_file
1222+
vim.api.nvim_get_current_buf = original_get_current_buf
1223+
vim.notify = original_notify
1224+
end)
1225+
1226+
it('should return nil and notify when no visual selection found', function()
1227+
local original_is_buf_a_file = util.is_buf_a_file
1228+
local original_get_current_selection = BaseContext.get_current_selection
1229+
local original_get_current_buf = vim.api.nvim_get_current_buf
1230+
1231+
util.is_buf_a_file = function()
1232+
return true
1233+
end
1234+
BaseContext.get_current_selection = function()
1235+
return nil
1236+
end
1237+
vim.api.nvim_get_current_buf = function()
1238+
return 11
1239+
end
1240+
1241+
local original_notify = vim.notify
1242+
local notifications = {}
1243+
vim.notify = function(msg, level)
1244+
table.insert(notifications, { msg = msg, level = level })
1245+
end
1246+
1247+
local text = context.build_inline_selection_text()
1248+
1249+
assert.is_nil(text)
1250+
assert.equal(1, #notifications)
1251+
assert.equal('No visual selection found', notifications[1].msg)
1252+
assert.equal(vim.log.levels.WARN, notifications[1].level)
1253+
1254+
util.is_buf_a_file = original_is_buf_a_file
1255+
BaseContext.get_current_selection = original_get_current_selection
1256+
vim.api.nvim_get_current_buf = original_get_current_buf
1257+
vim.notify = original_notify
1258+
end)
1259+
1260+
it('should include the filetype in the code fence', function()
1261+
local original_is_buf_a_file = util.is_buf_a_file
1262+
local original_get_current_selection = BaseContext.get_current_selection
1263+
local original_get_current_file_for_selection = BaseContext.get_current_file_for_selection
1264+
local original_get_current_buf = vim.api.nvim_get_current_buf
1265+
1266+
util.is_buf_a_file = function()
1267+
return true
1268+
end
1269+
BaseContext.get_current_selection = function()
1270+
return { text = 'const x = 1', lines = '1, 1' }
1271+
end
1272+
BaseContext.get_current_file_for_selection = function()
1273+
return { path = '/tmp/app.ts', name = 'app.ts', extension = 'ts' }
1274+
end
1275+
vim.api.nvim_get_current_buf = function()
1276+
return 6
1277+
end
1278+
1279+
local original_bo = vim.bo
1280+
vim.bo = setmetatable({}, {
1281+
__index = function(_, buf)
1282+
return { filetype = 'typescript' }
1283+
end,
1284+
})
1285+
1286+
local text = context.build_inline_selection_text()
1287+
1288+
assert.is_not_nil(text)
1289+
assert.is_not_nil(text:match('```typescript'))
1290+
1291+
util.is_buf_a_file = original_is_buf_a_file
1292+
BaseContext.get_current_selection = original_get_current_selection
1293+
BaseContext.get_current_file_for_selection = original_get_current_file_for_selection
1294+
vim.api.nvim_get_current_buf = original_get_current_buf
1295+
vim.bo = original_bo
1296+
end)
1297+
end)

tests/unit/output_window_spec.lua

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
local config = require('opencode.config')
2+
local output_window = require('opencode.ui.output_window')
3+
4+
describe('output_window.create_buf', function()
5+
local original_config
6+
7+
before_each(function()
8+
original_config = vim.deepcopy(config.values)
9+
config.values = vim.deepcopy(config.defaults)
10+
end)
11+
12+
after_each(function()
13+
config.values = original_config
14+
end)
15+
16+
it('uses default output filetype', function()
17+
config.setup({})
18+
local buf = output_window.create_buf()
19+
20+
local filetype = vim.api.nvim_get_option_value('filetype', { buf = buf })
21+
assert.equals('opencode_output', filetype)
22+
23+
pcall(vim.api.nvim_buf_delete, buf, { force = true })
24+
end)
25+
26+
it('uses configured output filetype', function()
27+
config.setup({
28+
ui = {
29+
output = {
30+
filetype = 'markdown',
31+
},
32+
},
33+
})
34+
35+
local buf = output_window.create_buf()
36+
local filetype = vim.api.nvim_get_option_value('filetype', { buf = buf })
37+
38+
assert.equals('markdown', filetype)
39+
40+
pcall(vim.api.nvim_buf_delete, buf, { force = true })
41+
end)
42+
end)

0 commit comments

Comments
 (0)