Skip to content

Commit cb70f9a

Browse files
committed
wip: context bar
1 parent bebe01c commit cb70f9a

20 files changed

+835
-81
lines changed

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
@@ -422,6 +424,18 @@ The following editor context is automatically captured and included in your conv
422424
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.
423425
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)
424426

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

427441
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
@@ -147,6 +147,7 @@ M.defaults = {
147147
enabled = false,
148148
},
149149
diagnostics = {
150+
enabled = false,
150151
info = false,
151152
warning = true,
152153
error = true,
@@ -162,6 +163,9 @@ M.defaults = {
162163
selection = {
163164
enabled = true,
164165
},
166+
agents = {
167+
enabled = true,
168+
},
165169
},
166170
debug = {
167171
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)
@@ -113,9 +124,19 @@ function M.add_file(file)
113124
end
114125
end
115126

116-
M.clear_files = function()
117-
M.context.mentioned_files = nil
118-
M.context.mentioned_files_content = nil
127+
function M.remove_file(file)
128+
file = vim.fn.fnamemodify(file, ':p')
129+
if not M.context.mentioned_files then
130+
return
131+
end
132+
133+
for i, f in ipairs(M.context.mentioned_files) do
134+
if f == file then
135+
table.remove(M.context.mentioned_files, i)
136+
break
137+
end
138+
end
139+
state.context_updated_at = vim.uv.now()
119140
end
120141

121142
function M.add_subagent(subagent)
@@ -126,13 +147,24 @@ function M.add_subagent(subagent)
126147
if not vim.tbl_contains(M.context.mentioned_subagents, subagent) then
127148
table.insert(M.context.mentioned_subagents, subagent)
128149
end
150+
state.context_updated_at = vim.uv.now()
129151
end
130152

131-
M.clear_subagents = function()
132-
M.context.mentioned_subagents = nil
153+
function M.remove_subagent(subagent)
154+
if not M.context.mentioned_subagents then
155+
return
156+
end
157+
158+
for i, a in ipairs(M.context.mentioned_subagents) do
159+
if a == subagent then
160+
table.remove(M.context.mentioned_subagents, i)
161+
break
162+
end
163+
end
164+
state.context_updated_at = vim.uv.now()
133165
end
134166

135-
---@param opts OpencodeContextConfig
167+
---@param opts? OpencodeContextConfig
136168
function M.delta_context(opts)
137169
opts = opts or config.context
138170
if opts.enabled == false then
@@ -173,18 +205,11 @@ function M.delta_context(opts)
173205
return context
174206
end
175207

176-
function M.get_current_file()
177-
if
178-
not (
179-
config.context
180-
and config.context.enabled
181-
and config.context.current_file
182-
and config.context.current_file.enabled
183-
)
184-
then
208+
function M.get_current_file(buf)
209+
if not M.is_context_enabled('current_file') then
185210
return nil
186211
end
187-
local file = vim.fn.expand('%:p')
212+
local file = vim.api.nvim_buf_get_name(buf)
188213
if not file or file == '' or vim.fn.filereadable(file) ~= 1 then
189214
return nil
190215
end
@@ -195,29 +220,21 @@ function M.get_current_file()
195220
}
196221
end
197222

198-
function M.get_current_cursor_data()
199-
if
200-
not (
201-
config.context
202-
and config.context.enabled
203-
and config.context.cursor_data
204-
and config.context.cursor_data.enabled
205-
)
206-
then
223+
function M.get_current_cursor_data(buf, win)
224+
if not M.is_context_enabled('cursor_data') then
207225
return nil
208226
end
209227

210-
local cursor_pos = vim.fn.getcurpos()
211-
local cursor_content = vim.trim(vim.api.nvim_get_current_line())
228+
local cursor_pos = vim.fn.getcurpos(win)
229+
local cursor_content = vim.trim(vim.api.nvim_buf_get_lines(buf, cursor_pos[2] - 1, cursor_pos[2], false)[1] or '')
212230
return { line = cursor_pos[2], col = cursor_pos[3], line_content = cursor_content }
213231
end
214232

215233
function M.get_current_selection()
216-
if
217-
not (config.context and config.context.enabled and config.context.selection and config.context.selection.enabled)
218-
then
234+
if not M.is_context_enabled('selection') then
219235
return nil
220236
end
237+
221238
-- Return nil if not in a visual mode
222239
if not vim.fn.mode():match('[vV\022]') then
223240
return nil
@@ -411,8 +428,6 @@ function M.extract_legacy_tag(tag, text)
411428
local start_tag = '<' .. tag .. '>'
412429
local end_tag = '</' .. tag .. '>'
413430

414-
-- Use pattern matching to find the content between the tags
415-
-- Make search start_tag and end_tag more robust with pattern escaping
416431
local pattern = vim.pesc(start_tag) .. '(.-)' .. vim.pesc(end_tag)
417432
local content = text:match(pattern)
418433

@@ -425,12 +440,39 @@ function M.extract_legacy_tag(tag, text)
425440
local query_end = text:find(end_tag)
426441

427442
if query_start and query_end then
428-
-- Extract and trim the content between the tags
429443
local query_content = text:sub(query_start + #start_tag, query_end - 1)
430444
return vim.trim(query_content)
431445
end
432446

433447
return nil
434448
end
435449

450+
function M.setup()
451+
state.subscribe({ 'current_code_buf', 'current_context_config', 'opencode_focused' }, function()
452+
M.load()
453+
end)
454+
455+
vim.api.nvim_create_autocmd('BufWritePost', {
456+
pattern = '*',
457+
callback = function(args)
458+
local buf = args.buf
459+
local curr_buf = state.current_code_buf or vim.api.nvim_get_current_buf()
460+
if buf == curr_buf and util.is_buf_a_file(buf) then
461+
M.load()
462+
end
463+
end,
464+
})
465+
466+
vim.api.nvim_create_autocmd('DiagnosticChanged', {
467+
pattern = '*',
468+
callback = function(args)
469+
local buf = args.buf
470+
local curr_buf = state.current_code_buf or vim.api.nvim_get_current_buf()
471+
if buf == curr_buf and util.is_buf_a_file(buf) and M.is_context_enabled('diagnostics') then
472+
M.load()
473+
end
474+
end,
475+
})
476+
end
477+
436478
return M

0 commit comments

Comments
 (0)