33local util = require (' opencode.util' )
44local config = require (' opencode.config' )
55local state = require (' opencode.state' )
6- local func = require (' vim.func ' )
6+ local Promise = require (' opencode.promise ' )
77
88local M = {}
99
@@ -41,10 +41,12 @@ function ContextInstance:unload_attachments()
4141 self .context .linter_errors = nil
4242end
4343
44+ --- @return integer | nil , integer | nil
4445function 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
4951end
5052
@@ -67,6 +69,20 @@ function ContextInstance:load()
6769 end
6870end
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+ }
280336end
281337
282338function ContextInstance :get_current_selection ()
@@ -327,6 +383,14 @@ function ContextInstance:get_selections()
327383 return self .context .selections or {}
328384end
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
332396function 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 }
543610end
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 }
587666end
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