Skip to content

Commit f94f6a3

Browse files
committed
feat: optimize SEARCH/REPLACE quick chat for concise context and output rules
- Refactor quick chat SEARCH/REPLACE message setup for stricter Aider-style output - Only emit SEARCH/REPLACE blocks, with clear output and matching rules - Remove buffer context from quick chat, improving token efficiency, can be added with #buffer - Centralize and clarify context-specific guidance and dynamic rule composition
1 parent c69581e commit f94f6a3

File tree

3 files changed

+56
-129
lines changed

3 files changed

+56
-129
lines changed

lua/opencode/context.lua

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
local util = require('opencode.util')
66
local config = require('opencode.config')
77
local state = require('opencode.state')
8-
local Promise = require('opencode.promise')
98

10-
-- Import extracted modules
119
local ContextInstance = require('opencode.context.base')
1210
local json_formatter = require('opencode.context.json_formatter')
1311
local plain_text_formatter = require('opencode.context.plain_text_formatter')
@@ -136,13 +134,6 @@ function M.get_current_selection()
136134
return global_context:get_current_selection()
137135
end
138136

139-
--- Formats context as plain text for LLM consumption
140-
--- Outputs human-readable text instead of JSON message parts
141-
--- Alias: format_message_quick_chat
142-
---@param prompt string The user's instruction/prompt
143-
---@param context_instance ContextInstance Context instance to use
144-
---@param opts? { range?: { start: integer, stop: integer }, buf?: integer }
145-
---@return table result { text: string, parts: OpencodeMessagePart[] }
146137
M.format_message_plain_text = plain_text_formatter.format_message
147138

148139
--- Formats a prompt and context into message with parts for the opencode API
@@ -185,12 +176,6 @@ function M.format_message(prompt, opts)
185176
return parts
186177
end
187178

188-
--- Formats a prompt and context into plain text message for quick chat
189-
--- Alias for format_message_plain_text - used for ephemeral sessions
190-
---@param prompt string
191-
---@param context_instance ContextInstance Context instance to use
192-
---@param opts? { range?: { start: integer, stop: integer }, buf?: integer }
193-
---@return table result { text: string, parts: OpencodeMessagePart[] }
194179
M.format_message_quick_chat = plain_text_formatter.format_message
195180

196181
---@param text string

lua/opencode/context/plain_text_formatter.lua

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,26 @@ function M.format_buffer(buf, lang)
105105
return string.format('FILE: %s\n\n```%s\n%s\n```', rel_path, lang, content)
106106
end
107107

108+
---@param buf integer
109+
---@param lang string
110+
---@param rel_path string
111+
---@param range {start: integer, stop: integer}
112+
---@return string
113+
function M.format_range(buf, lang, rel_path, range)
114+
local start_line = math.max(1, range.start)
115+
local end_line = range.stop
116+
local range_lines = vim.api.nvim_buf_get_lines(buf, start_line - 1, end_line, false)
117+
local range_text = table.concat(range_lines, '\n')
118+
119+
local parts = {
120+
string.format('Selected range from %s (lines %d-%d):', rel_path, start_line, end_line),
121+
'```' .. lang,
122+
range_text,
123+
'```',
124+
}
125+
return table.concat(parts, '\n')
126+
end
127+
108128
--- Formats context as plain text for LLM consumption (used by quick chat)
109129
--- Unlike format_message_quick_chat, this outputs human-readable text instead of JSON
110130
---@param prompt string The user's instruction/prompt
@@ -113,7 +133,9 @@ end
113133
---@return table result { text: string, parts: OpencodeMessagePart[] }
114134
M.format_message = Promise.async(function(prompt, context_instance, opts)
115135
opts = opts or {}
116-
local buf = opts.buf or context_instance:get_current_buf() or vim.api.nvim_get_current_buf()
136+
local buf = vim.api.nvim_get_current_buf()
137+
local win = vim.api.nvim_get_current_win()
138+
117139
local range = opts.range
118140

119141
local file_name = vim.api.nvim_buf_get_name(buf)
@@ -122,24 +144,17 @@ M.format_message = Promise.async(function(prompt, context_instance, opts)
122144

123145
local text_parts = {}
124146

125-
-- Add file/buffer content
126-
if context_instance:is_context_enabled('buffer') then
147+
if context_instance:is_context_enabled('selection') then
127148
if range and range.start and range.stop then
128-
local start_line = math.max(1, range.start)
129-
local end_line = range.stop
130-
local range_lines = vim.api.nvim_buf_get_lines(buf, start_line - 1, end_line, false)
131-
local range_text = table.concat(range_lines, '\n')
132-
133-
table.insert(text_parts, string.format('FILE: %s (lines %d-%d)', rel_path, start_line, end_line))
134149
table.insert(text_parts, '')
135-
table.insert(text_parts, '```' .. lang)
136-
table.insert(text_parts, range_text)
137-
table.insert(text_parts, '```')
138-
else
139-
table.insert(text_parts, M.format_buffer(buf, lang))
150+
table.insert(text_parts, M.format_range(buf, lang, rel_path, range))
140151
end
141152
end
142153

154+
if context_instance:is_context_enabled('buffer') then
155+
table.insert(text_parts, M.format_buffer(buf, lang))
156+
end
157+
143158
for _, sel in ipairs(context_instance:get_selections() or {}) do
144159
table.insert(text_parts, '')
145160
table.insert(text_parts, M.format_selection(sel))
@@ -159,8 +174,7 @@ M.format_message = Promise.async(function(prompt, context_instance, opts)
159174
end
160175

161176
if context_instance:is_context_enabled('cursor_data') then
162-
local current_buf, current_win = context_instance:get_current_buf()
163-
local cursor_data = context_instance:get_current_cursor_data(current_buf or buf, current_win or 0)
177+
local cursor_data = context_instance:get_current_cursor_data(buf, win)
164178
if cursor_data then
165179
table.insert(text_parts, '')
166180
table.insert(text_parts, M.format_cursor_data(cursor_data, lang))

lua/opencode/quick_chat.lua

Lines changed: 26 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -167,135 +167,66 @@ local function validate_quick_chat_prerequisites(message)
167167
end
168168

169169
--- Creates context configuration for quick chat
170+
--- Optimized for minimal token usage while providing essential context
170171
---@param has_range boolean Whether a range is specified
171172
---@return OpencodeContextConfig context_opts
172173
local function create_context_config(has_range)
173174
return {
174175
enabled = true,
175-
current_file = { enabled = false },
176-
cursor_data = { enabled = not has_range },
177-
selection = { enabled = has_range },
176+
current_file = { enabled = false }, -- Disable full file content
177+
cursor_data = { enabled = not has_range, context_lines = 10 }, -- Only cursor position when no selection
178+
selection = { enabled = has_range }, -- Only selected text when range provided
178179
diagnostics = {
179180
enabled = true,
180181
error = true,
181182
warning = true,
182183
info = false,
183-
only_closest = has_range,
184+
only_closest = true, -- Only closest diagnostics, not all file diagnostics
184185
},
185-
agents = { enabled = false },
186-
buffer = { enabled = true },
187-
git_diff = { enabled = false },
186+
agents = { enabled = false }, -- No agent context needed
187+
buffer = { enabled = false }, -- Disable full buffer content for token efficiency
188+
git_diff = { enabled = false }, -- No git context needed
188189
}
189190
end
190191

191192
--- Generates instructions for the LLM to follow the SEARCH/REPLACE format
193+
--- This is inspired from Aider Chat approach
192194
---@param context_instance ContextInstance Context instance
193195
---@return string[] instructions Array of instruction lines
194-
local function generate_search_replace_instructions(context_instance)
196+
local generate_search_replace_instructions = Promise.async(function(context_instance)
195197
local base_instructions = {
196-
'# ROLE',
197-
'You are a precise code editing assistant. Your task is to modify code based on user instructions.',
198-
'',
199-
'# OUTPUT FORMAT',
200-
'You MUST output ONLY in SEARCH/REPLACE blocks. No explanations, no markdown, no additional text.',
201-
'',
198+
'Output ONLY SEARCH/REPLACE blocks, no explanations:',
202199
'<<<<<<< SEARCH',
203-
'[exact lines from the original code]',
200+
'[exact original code]',
204201
'=======',
205-
'[modified version of those lines]',
202+
'[modified code]',
206203
'>>>>>>> REPLACE',
207204
'',
208-
'# CRITICAL RULES',
209-
'1. **Exact matching**: Copy SEARCH content EXACTLY character-for-character from the provided code',
210-
'2. **Context lines**: Include 1-3 unchanged surrounding lines in SEARCH for unique identification',
211-
'3. **Indentation**: Preserve the exact indentation from the original code',
212-
'4. **Multiple changes**: Use separate SEARCH/REPLACE blocks for each distinct change',
213-
'5. **No explanations**: Output ONLY the SEARCH/REPLACE blocks, nothing else',
214-
'',
205+
'Rules: Copy SEARCH content exactly. Include 1-3 context lines for unique matching. Use empty SEARCH to insert at cursor. Output multiple blocks only if needed for more complex operations.',
215206
}
216207

217-
-- Add context-specific guidance
218208
local context_guidance = {}
219209

220-
if context_instance:has('diagnostics') then
221-
table.insert(context_guidance, '**DIAGNOSTICS context**: Use error/warning information to guide your fixes')
210+
if context_instance:has('diagnostics'):await() then
211+
table.insert(context_guidance, 'Fix errors/warnings (if asked)')
222212
end
223213

224-
if context_instance:has('selection') then
225-
table.insert(context_guidance, '**SELECTION context**: Only modify code within the selected range')
214+
if context_instance:has('selection'):await() then
215+
table.insert(context_guidance, 'Modify only selected range')
226216
elseif context_instance:has('cursor_data') then
227-
table.insert(context_guidance, '**CURSOR context**: Focus modifications near the cursor position')
217+
table.insert(context_guidance, 'Modify only near cursor')
228218
end
229219

230-
if context_instance:has('git_diff') then
231-
table.insert(context_guidance, '**GIT_DIFF context**: For reference only - never copy git diff syntax into SEARCH')
220+
if context_instance:has('git_diff'):await() then
221+
table.insert(context_guidance, "ONLY Reference git diff (don't copy syntax)")
232222
end
233223

234224
if #context_guidance > 0 then
235-
table.insert(base_instructions, '# CONTEXT USAGE')
236-
for _, guidance in ipairs(context_guidance) do
237-
table.insert(base_instructions, '- ' .. guidance)
238-
end
239-
table.insert(base_instructions, '')
240-
end
241-
242-
-- Add practical examples
243-
local examples = {
244-
'# EXAMPLES',
245-
'',
246-
'**Modify a return value:**',
247-
'<<<<<<< SEARCH',
248-
'function calculate()',
249-
' local result = x + y',
250-
' return result * 2',
251-
'end',
252-
'=======',
253-
'function calculate()',
254-
' local result = x + y',
255-
' return result * 3 -- Changed multiplier',
256-
'end',
257-
'>>>>>>> REPLACE',
258-
'',
259-
'**Insert a new line:**',
260-
'<<<<<<< SEARCH',
261-
'local config = {',
262-
' timeout = 5000,',
263-
'}',
264-
'=======',
265-
'local config = {',
266-
' timeout = 5000,',
267-
' retry_count = 3,',
268-
'}',
269-
'>>>>>>> REPLACE',
270-
'',
271-
'**Remove a line:**',
272-
'<<<<<<< SEARCH',
273-
'local debug_mode = true',
274-
'local verbose = true',
275-
'local silent = false',
276-
'=======',
277-
'local debug_mode = true',
278-
'local silent = false',
279-
'>>>>>>> REPLACE',
280-
'',
281-
'**Insert new code at cursor (empty SEARCH):**',
282-
'When the cursor is on an empty line or you need to insert without replacing, use an empty SEARCH section:',
283-
'<<<<<<< SEARCH',
284-
'=======',
285-
'local new_variable = "inserted at cursor"',
286-
'>>>>>>> REPLACE',
287-
'',
288-
'# FINAL REMINDER',
289-
'Output ONLY the SEARCH/REPLACE blocks. The SEARCH section must match the original code exactly.',
290-
'Use an empty SEARCH section to insert new code at the cursor position.',
291-
}
292-
293-
for _, line in ipairs(examples) do
294-
table.insert(base_instructions, line)
225+
table.insert(base_instructions, 'Context: ' .. table.concat(context_guidance, ', ') .. '.')
295226
end
296227

297228
return base_instructions
298-
end
229+
end)
299230

300231
--- Creates message parameters for quick chat
301232
---@param message string The user message
@@ -311,7 +242,7 @@ local create_message = Promise.async(function(message, buf, range, context_insta
311242
if quick_chat_config.instructions then
312243
instructions = quick_chat_config.instructions
313244
else
314-
instructions = generate_search_replace_instructions(context_instance)
245+
instructions = generate_search_replace_instructions(context_instance):await()
315246
end
316247

317248
local format_opts = { buf = buf }
@@ -365,11 +296,6 @@ M.quick_chat = Promise.async(function(message, options, range)
365296
local row, col = cursor_pos[1] - 1, cursor_pos[2] -- Convert to 0-indexed
366297
local spinner = CursorSpinner.new(buf, row, col)
367298

368-
-- Create context instance for diagnostics and other context
369-
local context_config = vim.tbl_deep_extend('force', create_context_config(range ~= nil), options.context_config or {})
370-
local context_instance = context.new_instance(context_config)
371-
372-
-- Check prompt guard with the current file
373299
local file_name = vim.api.nvim_buf_get_name(buf)
374300
local mentioned_files = file_name ~= '' and { file_name } or {}
375301
local allowed, err_msg = util.check_prompt_allowed(config.values.prompt_guard, mentioned_files)
@@ -396,6 +322,8 @@ M.quick_chat = Promise.async(function(message, options, range)
396322
timestamp = vim.uv.now(),
397323
}
398324

325+
local context_config = vim.tbl_deep_extend('force', create_context_config(range ~= nil), options.context_config or {})
326+
local context_instance = context.new_instance(context_config)
399327
local params = create_message(message, buf, range, context_instance, options):await()
400328

401329
local success, err = pcall(function()

0 commit comments

Comments
 (0)