Skip to content

Commit a512f0f

Browse files
committed
feat: new buffer context types
- git_diff: the current - current buffer: the current buffer instead of relying on the file reading (can chat with unsaved buffer)
1 parent cbd3fd5 commit a512f0f

File tree

7 files changed

+672
-203
lines changed

7 files changed

+672
-203
lines changed

lua/opencode/config.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,14 @@ M.defaults = {
166166
enabled = true,
167167
cursor_data = {
168168
enabled = false,
169-
context_lines = 10, -- Number of lines before and after cursor to include in context
169+
context_lines = 5, -- Number of lines before and after cursor to include in context
170170
},
171171
diagnostics = {
172172
enabled = true,
173173
info = false,
174174
warning = true,
175175
error = true,
176+
only_closest = false, -- If true, only diagnostics for cursor/selection
176177
},
177178
current_file = {
178179
enabled = true,
@@ -188,6 +189,12 @@ M.defaults = {
188189
agents = {
189190
enabled = true,
190191
},
192+
buffer = {
193+
enabled = false, -- Only used for inline editing, disabled by default
194+
},
195+
git_diff = {
196+
enabled = false,
197+
},
191198
},
192199
debug = {
193200
enabled = false,

lua/opencode/context.lua

Lines changed: 138 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
local util = require('opencode.util')
44
local config = require('opencode.config')
55
local state = require('opencode.state')
6-
local func = require('vim.func')
6+
local Promise = require('opencode.promise')
77

88
local M = {}
99

@@ -41,10 +41,12 @@ function ContextInstance:unload_attachments()
4141
self.context.linter_errors = nil
4242
end
4343

44+
---@return integer|nil, integer|nil
4445
function ContextInstance:get_current_buf()
4546
local curr_buf = state.current_code_buf or vim.api.nvim_get_current_buf()
4647
if util.is_buf_a_file(curr_buf) then
47-
return curr_buf, state.last_code_win_before_opencode or vim.api.nvim_get_current_win()
48+
local win = vim.fn.win_findbuf(curr_buf --[[@as integer]])[1]
49+
return curr_buf, state.last_code_win_before_opencode or win or vim.api.nvim_get_current_win()
4850
end
4951
end
5052

@@ -67,6 +69,20 @@ function ContextInstance:load()
6769
end
6870
end
6971

72+
function ContextInstance:is_enabled()
73+
if self.context_config and self.context_config.enabled ~= nil then
74+
return self.context_config.enabled
75+
end
76+
77+
local is_enabled = vim.tbl_get(config --[[@as table]], 'context', 'enabled')
78+
local is_state_enabled = vim.tbl_get(state, 'current_context_config', 'enabled')
79+
if is_state_enabled ~= nil then
80+
return is_state_enabled
81+
else
82+
return is_enabled
83+
end
84+
end
85+
7086
-- Checks if a context feature is enabled in config or state
7187
---@param context_key string
7288
---@return boolean
@@ -116,12 +132,42 @@ function ContextInstance:get_diagnostics(buf)
116132
table.insert(severity_levels, vim.diagnostic.severity.INFO)
117133
end
118134

119-
local diagnostics = vim.diagnostic.get(buf, { severity = severity_levels })
135+
local diagnostics = {}
136+
if diagnostic_conf.only_closest then
137+
local selections = self:get_selections()
138+
if #selections > 0 then
139+
local selection = selections[#selections]
140+
if selection and selection.lines then
141+
local range_parts = vim.split(selection.lines, ',')
142+
local start_line = (tonumber(range_parts[1]) or 1) - 1
143+
local end_line = (tonumber(range_parts[2]) or 1) - 1
144+
for lnum = start_line, end_line do
145+
local line_diagnostics = vim.diagnostic.get(buf, {
146+
lnum = lnum,
147+
severity = severity_levels,
148+
})
149+
for _, diag in ipairs(line_diagnostics) do
150+
table.insert(diagnostics, diag)
151+
end
152+
end
153+
end
154+
else
155+
local win = vim.fn.win_findbuf(buf)[1]
156+
local cursor_pos = vim.fn.getcurpos(win)
157+
local line_diagnostics = vim.diagnostic.get(buf, {
158+
lnum = cursor_pos[2] - 1,
159+
severity = severity_levels,
160+
})
161+
diagnostics = line_diagnostics
162+
end
163+
else
164+
diagnostics = vim.diagnostic.get(buf, { severity = severity_levels })
165+
end
166+
120167
if #diagnostics == 0 then
121168
return {}
122169
end
123170

124-
-- Convert vim.Diagnostic[] to OpencodeDiagnostic[]
125171
local opencode_diagnostics = {}
126172
for _, diag in ipairs(diagnostics) do
127173
table.insert(opencode_diagnostics, {
@@ -273,10 +319,20 @@ function ContextInstance:get_current_cursor_data(buf, win)
273319
return nil
274320
end
275321

322+
local num_lines = config.context.cursor_data.context_lines --[[@as integer]]
323+
or 0
276324
local cursor_pos = vim.fn.getcurpos(win)
277325
local start_line = (cursor_pos[2] - 1) --[[@as integer]]
278326
local cursor_content = vim.trim(vim.api.nvim_buf_get_lines(buf, start_line, cursor_pos[2], false)[1] or '')
279-
return { line = cursor_pos[2], column = cursor_pos[3], line_content = cursor_content }
327+
local lines_before = vim.api.nvim_buf_get_lines(buf, math.max(0, start_line - num_lines), start_line, false)
328+
local lines_after = vim.api.nvim_buf_get_lines(buf, cursor_pos[2], cursor_pos[2] + num_lines, false)
329+
return {
330+
line = cursor_pos[2],
331+
column = cursor_pos[3],
332+
line_content = cursor_content,
333+
lines_before = lines_before,
334+
lines_after = lines_after,
335+
}
280336
end
281337

282338
function ContextInstance:get_current_selection()
@@ -327,6 +383,14 @@ function ContextInstance:get_selections()
327383
return self.context.selections or {}
328384
end
329385

386+
ContextInstance.get_git_diff = Promise.async(function(self)
387+
if not self:is_context_enabled('git_diff') then
388+
return nil
389+
end
390+
391+
Promise.system({ 'git', 'diff', '--cached' })
392+
end)
393+
330394
---@param opts? OpencodeContextConfig
331395
---@return OpencodeContext
332396
function ContextInstance:delta_context(opts)
@@ -532,28 +596,37 @@ local function format_selection_part(selection)
532596

533597
return {
534598
type = 'text',
599+
metadata = {
600+
context_type = 'selection',
601+
},
535602
text = vim.json.encode({
536603
context_type = 'selection',
537604
file = selection.file,
538-
content = string.format('`````%s\n%s\n`````', lang, selection.content),
605+
content = string.format('`````%s\n%s\n`````', lang, selection.content), --@TODO remove code fence and only use it when displaying
539606
lines = selection.lines,
540607
}),
541608
synthetic = true,
542609
}
543610
end
544611

545612
---@param diagnostics OpencodeDiagnostic[]
546-
local function format_diagnostics_part(diagnostics)
613+
---@param range? { start_line: integer, end_line: integer }|nil
614+
local function format_diagnostics_part(diagnostics, range)
547615
local diag_list = {}
548616
for _, diag in ipairs(diagnostics) do
549-
local short_msg = diag.message:gsub('%s+', ' '):gsub('^%s', ''):gsub('%s$', '')
550-
table.insert(
551-
diag_list,
552-
{ msg = short_msg, severity = diag.severity, pos = 'l' .. diag.lnum + 1 .. ':c' .. diag.col + 1 }
553-
)
617+
if not range or (diag.lnum >= range.start_line and diag.lnum <= range.end_line) then
618+
local short_msg = diag.message:gsub('%s+', ' '):gsub('^%s', ''):gsub('%s$', '')
619+
table.insert(
620+
diag_list,
621+
{ msg = short_msg, severity = diag.severity, pos = 'l' .. diag.lnum + 1 .. ':c' .. diag.col + 1 }
622+
)
623+
end
554624
end
555625
return {
556626
type = 'text',
627+
meradata = {
628+
context_type = 'diagnostics',
629+
},
557630
text = vim.json.encode({ context_type = 'diagnostics', content = diag_list }),
558631
synthetic = true,
559632
}
@@ -564,11 +637,17 @@ local function format_cursor_data_part(cursor_data)
564637
local lang = util.get_markdown_filetype(vim.api.nvim_buf_get_name(buf)) or ''
565638
return {
566639
type = 'text',
640+
metadata = {
641+
context_type = 'cursor-data',
642+
lang = lang,
643+
},
567644
text = vim.json.encode({
568645
context_type = 'cursor-data',
569646
line = cursor_data.line,
570647
column = cursor_data.column,
571-
line_content = string.format('`````%s\n%s\n`````', lang, cursor_data.line_content),
648+
line_content = string.format('`````%s\n%s\n`````', lang, cursor_data.line_content), --@TODO remove code fence and only use it when displaying
649+
lines_before = cursor_data.lines_before,
650+
lines_after = cursor_data.lines_after,
572651
}),
573652
synthetic = true,
574653
}
@@ -586,6 +665,32 @@ local function format_subagents_part(agent, prompt)
586665
}
587666
end
588667

668+
local function format_buffer_part(buf)
669+
local file = vim.api.nvim_buf_get_name(buf)
670+
local rel_path = vim.fn.fnamemodify(file, ':~:.')
671+
return {
672+
type = 'text',
673+
text = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), '\n'),
674+
metadata = {
675+
context_type = 'file-content',
676+
filename = rel_path,
677+
mime = 'text/plain',
678+
},
679+
synthetic = true,
680+
}
681+
end
682+
683+
local function format_git_diff_part(diff_text)
684+
return {
685+
type = 'text',
686+
metadata = {
687+
context_type = 'git-diff',
688+
},
689+
text = diff_text,
690+
synthetic = true,
691+
}
692+
end
693+
589694
--- Formats a prompt and context into message with parts for the opencode API
590695
---@param prompt string
591696
---@param opts? OpencodeContextConfig|nil
@@ -629,22 +734,20 @@ end
629734
--- Formats a prompt and context into message without state tracking (bypasses delta)
630735
--- Used for ephemeral sessions like quick chat that don't track context state
631736
---@param prompt string
632-
---@param opts? OpencodeContextConfig|nil
633737
---@param context_instance ContextInstance Optional context instance to use instead of global
634738
---@return OpencodeMessagePart[]
635-
function M.format_message_stateless(prompt, opts, context_instance)
636-
opts = opts or config.context
637-
if opts.enabled == false then
638-
return { { type = 'text', text = prompt } }
639-
end
739+
M.format_message_quick_chat = Promise.async(function(prompt, context_instance)
640740
local parts = { { type = 'text', text = prompt } }
641741

742+
if context_instance:is_enabled() == false then
743+
return parts
744+
end
745+
642746
for _, path in ipairs(context_instance:get_mentioned_files() or {}) do
643747
table.insert(parts, format_file_part(path, prompt))
644748
end
645749

646750
for _, sel in ipairs(context_instance:get_selections() or {}) do
647-
vim.print('⭕ ❱ context.lua:639 ❱ ƒ(_) ❱ sel =', sel)
648751
table.insert(parts, format_selection_part(sel))
649752
end
650753

@@ -662,14 +765,26 @@ function M.format_message_stateless(prompt, opts, context_instance)
662765
table.insert(parts, format_diagnostics_part(diagnostics))
663766
end
664767

665-
local cursor_data =
666-
context_instance:get_current_cursor_data(context_instance:get_current_buf() or 0, vim.api.nvim_get_current_win())
768+
local current_buf, current_win = context_instance:get_current_buf()
769+
local cursor_data = context_instance:get_current_cursor_data(current_buf or 0, current_win or 0)
667770
if cursor_data then
668771
table.insert(parts, format_cursor_data_part(cursor_data))
669772
end
670773

774+
if context_instance:is_context_enabled('buffer') then
775+
local buf = context_instance:get_current_buf()
776+
if buf then
777+
table.insert(parts, format_buffer_part(buf))
778+
end
779+
end
780+
781+
local diff_text = context_instance:get_git_diff():await()
782+
if diff_text and diff_text ~= '' then
783+
table.insert(parts, format_git_diff_part(diff_text))
784+
end
785+
671786
return parts
672-
end
787+
end)
673788

674789
---@param text string
675790
---@param context_type string|nil

0 commit comments

Comments
 (0)