Skip to content

Commit 9dc7885

Browse files
committed
wip: context bar
1 parent 601cc82 commit 9dc7885

22 files changed

Lines changed: 875 additions & 89 deletions

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ require('opencode').setup({
132132
['~'] = { 'mention_file', mode = 'i' }, -- Pick a file and add to context. See File Mentions section
133133
['@'] = { 'mention', mode = 'i' }, -- Insert mention (file/agent)
134134
['/'] = { 'slash_commands', mode = 'i' }, -- Pick a command to run in the input window
135+
['#'] = { 'context_items', mode = 'i' }, -- Manage context items (current file, selection, diagnostics, mentioned files)
136+
['<C-i>'] = { 'focus_input', mode = { 'n', 'i' } }, -- Focus on input window and enter insert mode at the end of the input from the output window
135137
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' } }, -- Toggle between input and output panes
136138
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' } }, -- Navigate to previous prompt in history
137139
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' } }, -- Navigate to next prompt in history
@@ -415,6 +417,18 @@ The following editor context is automatically captured and included in your conv
415417
You can reference files in your project directly in your conversations with Opencode. This is useful when you want to ask about or provide context about specific files. Type `@` in the input window to trigger the file picker.
416418
Supported pickers include [`fzf-lua`](https://github.com/ibhagwan/fzf-lua), [`telescope`](https://github.com/nvim-telescope/telescope.nvim), [`mini.pick`](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-pick.md), [`snacks`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md)
417419

420+
### Context Items Completion
421+
422+
You can quickly reference available context items by typing `#` in the input window. This will show a completion menu with all available context items:
423+
424+
- **Current File** - The currently focused file in the editor
425+
- **Selection** - Currently selected text in visual mode
426+
- **Diagnostics** - LSP diagnostics from the current file
427+
- **Cursor Data** - Current cursor position and line content
428+
- **[filename]** - Files that have been mentioned in the conversation
429+
430+
Context items that are not currently available will be shown as disabled in the completion menu.
431+
418432
## 🔄 Agents
419433

420434
Opencode provides two built-in agents and supports custom ones:

lua/opencode/config.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ M.defaults = {
137137
enabled = false,
138138
},
139139
diagnostics = {
140+
enabled = false,
140141
info = false,
141142
warning = true,
142143
error = true,
@@ -152,6 +153,9 @@ M.defaults = {
152153
selection = {
153154
enabled = true,
154155
},
156+
agents = {
157+
enabled = true,
158+
},
155159
},
156160
debug = {
157161
enabled = false,

lua/opencode/context.lua

Lines changed: 98 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,58 @@ M.context = {
1313

1414
-- attachments
1515
mentioned_files = nil,
16-
mentioned_files_content = nil,
17-
selections = nil,
18-
linter_errors = nil,
19-
mentioned_subagents = nil,
16+
selections = {},
17+
linter_errors = {},
18+
mentioned_subagents = {},
2019
}
2120

2221
function M.unload_attachments()
2322
M.context.mentioned_files = nil
24-
M.context.mentioned_files_content = nil
2523
M.context.selections = nil
2624
M.context.linter_errors = nil
2725
end
2826

27+
function M.get_current_buf()
28+
local curr_buf = state.current_code_buf or vim.api.nvim_get_current_buf()
29+
if util.is_buf_a_file(curr_buf) then
30+
return curr_buf, state.last_code_win_before_opencode or vim.api.nvim_get_current_win()
31+
end
32+
end
33+
2934
function M.load()
30-
if util.is_current_buf_a_file() then
31-
local current_file = M.get_current_file()
32-
local cursor_data = M.get_current_cursor_data()
35+
local buf, win = M.get_current_buf()
36+
37+
if buf then
38+
local current_file = M.get_current_file(buf)
39+
local cursor_data = M.get_current_cursor_data(buf, win)
3340

3441
M.context.current_file = current_file
3542
M.context.cursor_data = cursor_data
36-
M.context.linter_errors = M.check_linter_errors()
43+
M.context.linter_errors = M.get_diagnostics(buf)
3744
end
3845

3946
local current_selection = M.get_current_selection()
4047
if current_selection then
4148
local selection = M.new_selection(M.context.current_file, current_selection.text, current_selection.lines)
4249
M.add_selection(selection)
4350
end
51+
state.context_updated_at = vim.uv.now()
4452
end
4553

46-
function M.check_linter_errors()
47-
local diagnostic_conf = config.context and config.context.diagnostics
48-
if not diagnostic_conf then
54+
function M.is_context_enabled(context_key)
55+
local is_enabled = vim.tbl_get(config, 'context', context_key, 'enabled')
56+
local is_state_enabled = vim.tbl_get(state, 'current_context_config', context_key, 'enabled')
57+
58+
return is_state_enabled ~= nil and is_state_enabled or is_enabled
59+
end
60+
61+
function M.get_diagnostics(buf)
62+
if not M.is_context_enabled('diagnostics') then
4963
return nil
5064
end
65+
66+
local diagnostic_conf = config.context and state.current_context_config.diagnostics or config.context.diagnostics
67+
5168
local severity_levels = {}
5269
if diagnostic_conf.error then
5370
table.insert(severity_levels, vim.diagnostic.severity.ERROR)
@@ -59,20 +76,12 @@ function M.check_linter_errors()
5976
table.insert(severity_levels, vim.diagnostic.severity.INFO)
6077
end
6178

62-
local diagnostics = vim.diagnostic.get(0, { severity = severity_levels })
79+
local diagnostics = vim.diagnostic.get(buf, { severity = severity_levels })
6380
if #diagnostics == 0 then
64-
return nil
65-
end
66-
67-
local lines = { 'Found ' .. #diagnostics .. ' error' .. (#diagnostics > 1 and 's' or '') .. ':' }
68-
69-
for _, diagnostic in ipairs(diagnostics) do
70-
local line_number = diagnostic.lnum + 1
71-
local short_message = diagnostic.message:gsub('%s+', ' '):gsub('^%s', ''):gsub('%s$', '')
72-
table.insert(lines, string.format(' Line %d: %s', line_number, short_message))
81+
return {}
7382
end
7483

75-
return table.concat(lines, '\n')
84+
return diagnostics
7685
end
7786

7887
function M.new_selection(file, content, lines)
@@ -89,6 +98,8 @@ function M.add_selection(selection)
8998
end
9099

91100
table.insert(M.context.selections, selection)
101+
102+
state.context_updated_at = vim.uv.now()
92103
end
93104

94105
function M.add_file(file)
@@ -108,9 +119,19 @@ function M.add_file(file)
108119
end
109120
end
110121

111-
M.clear_files = function()
112-
M.context.mentioned_files = nil
113-
M.context.mentioned_files_content = nil
122+
function M.remove_file(file)
123+
file = vim.fn.fnamemodify(file, ':p')
124+
if not M.context.mentioned_files then
125+
return
126+
end
127+
128+
for i, f in ipairs(M.context.mentioned_files) do
129+
if f == file then
130+
table.remove(M.context.mentioned_files, i)
131+
break
132+
end
133+
end
134+
state.context_updated_at = vim.uv.now()
114135
end
115136

116137
function M.add_subagent(subagent)
@@ -121,13 +142,24 @@ function M.add_subagent(subagent)
121142
if not vim.tbl_contains(M.context.mentioned_subagents, subagent) then
122143
table.insert(M.context.mentioned_subagents, subagent)
123144
end
145+
state.context_updated_at = vim.uv.now()
124146
end
125147

126-
M.clear_subagents = function()
127-
M.context.mentioned_subagents = nil
148+
function M.remove_subagent(subagent)
149+
if not M.context.mentioned_subagents then
150+
return
151+
end
152+
153+
for i, a in ipairs(M.context.mentioned_subagents) do
154+
if a == subagent then
155+
table.remove(M.context.mentioned_subagents, i)
156+
break
157+
end
158+
end
159+
state.context_updated_at = vim.uv.now()
128160
end
129161

130-
---@param opts OpencodeContextConfig
162+
---@param opts? OpencodeContextConfig
131163
function M.delta_context(opts)
132164
opts = opts or config.context
133165
if opts.enabled == false then
@@ -168,18 +200,11 @@ function M.delta_context(opts)
168200
return context
169201
end
170202

171-
function M.get_current_file()
172-
if
173-
not (
174-
config.context
175-
and config.context.enabled
176-
and config.context.current_file
177-
and config.context.current_file.enabled
178-
)
179-
then
203+
function M.get_current_file(buf)
204+
if not M.is_context_enabled('current_file') then
180205
return nil
181206
end
182-
local file = vim.fn.expand('%:p')
207+
local file = vim.api.nvim_buf_get_name(buf)
183208
if not file or file == '' or vim.fn.filereadable(file) ~= 1 then
184209
return nil
185210
end
@@ -190,29 +215,21 @@ function M.get_current_file()
190215
}
191216
end
192217

193-
function M.get_current_cursor_data()
194-
if
195-
not (
196-
config.context
197-
and config.context.enabled
198-
and config.context.cursor_data
199-
and config.context.cursor_data.enabled
200-
)
201-
then
218+
function M.get_current_cursor_data(buf, win)
219+
if not M.is_context_enabled('cursor_data') then
202220
return nil
203221
end
204222

205-
local cursor_pos = vim.fn.getcurpos()
206-
local cursor_content = vim.trim(vim.api.nvim_get_current_line())
223+
local cursor_pos = vim.fn.getcurpos(win)
224+
local cursor_content = vim.trim(vim.api.nvim_buf_get_lines(buf, cursor_pos[2] - 1, cursor_pos[2], false)[1] or '')
207225
return { line = cursor_pos[2], col = cursor_pos[3], line_content = cursor_content }
208226
end
209227

210228
function M.get_current_selection()
211-
if
212-
not (config.context and config.context.enabled and config.context.selection and config.context.selection.enabled)
213-
then
229+
if not M.is_context_enabled('selection') then
214230
return nil
215231
end
232+
216233
-- Return nil if not in a visual mode
217234
if not vim.fn.mode():match('[vV\022]') then
218235
return nil
@@ -403,8 +420,6 @@ function M.extract_legacy_tag(tag, text)
403420
local start_tag = '<' .. tag .. '>'
404421
local end_tag = '</' .. tag .. '>'
405422

406-
-- Use pattern matching to find the content between the tags
407-
-- Make search start_tag and end_tag more robust with pattern escaping
408423
local pattern = vim.pesc(start_tag) .. '(.-)' .. vim.pesc(end_tag)
409424
local content = text:match(pattern)
410425

@@ -417,12 +432,39 @@ function M.extract_legacy_tag(tag, text)
417432
local query_end = text:find(end_tag)
418433

419434
if query_start and query_end then
420-
-- Extract and trim the content between the tags
421435
local query_content = text:sub(query_start + #start_tag, query_end - 1)
422436
return vim.trim(query_content)
423437
end
424438

425439
return nil
426440
end
427441

442+
function M.setup()
443+
state.subscribe({ 'current_code_buf', 'current_context_config', 'opencode_focused' }, function()
444+
M.load()
445+
end)
446+
447+
vim.api.nvim_create_autocmd('BufWritePost', {
448+
pattern = '*',
449+
callback = function(args)
450+
local buf = args.buf
451+
local curr_buf = state.current_code_buf or vim.api.nvim_get_current_buf()
452+
if buf == curr_buf and util.is_buf_a_file(buf) then
453+
M.load()
454+
end
455+
end,
456+
})
457+
458+
vim.api.nvim_create_autocmd('DiagnosticChanged', {
459+
pattern = '*',
460+
callback = function(args)
461+
local buf = args.buf
462+
local curr_buf = state.current_code_buf or vim.api.nvim_get_current_buf()
463+
if buf == curr_buf and util.is_buf_a_file(buf) and M.is_context_enabled('diagnostics') then
464+
M.load()
465+
end
466+
end,
467+
})
468+
end
469+
428470
return M

lua/opencode/init.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@ local config = require('opencode.config')
33
local keymap = require('opencode.keymap')
44
local api = require('opencode.api')
55
local config_file = require('opencode.config_file')
6+
local context = require('opencode.context')
67

78
function M.setup(opts)
89
vim.schedule(function()
910
require('opencode.core').setup()
1011
config.setup(opts)
1112
api.setup()
1213
keymap.setup(config.keymap)
13-
1414
require('opencode.ui.completion').setup()
15+
local ui_conf = config.get('ui')
16+
if ui_conf.display_context_size or ui_conf.display_cost then
17+
require('opencode.models').setup()
18+
end
19+
require('opencode.ui.context_bar').setup()
1520
require('opencode.event_manager').setup()
21+
require('opencode.ui.context_bar').setup()
22+
context.setup()
1623
end)
1724
end
1825

0 commit comments

Comments
 (0)