Skip to content

Commit 9020c78

Browse files
committed
feat: replace search/replace with raw code generation in quick chat
Remove SEARCH/REPLACE block parsing in favor of direct code output mode for simpler implementation and improved user experience
1 parent 60d6d5e commit 9020c78

File tree

5 files changed

+64
-987
lines changed

5 files changed

+64
-987
lines changed

lua/opencode/api.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ M.toggle = Promise.async(function(new_session)
6060
end
6161
end)
6262

63+
---@param new_session boolean?
64+
---@return nil
6365
function M.toggle_focus(new_session)
6466
if not ui.is_opencode_focused() then
6567
local focus = state.last_focused_opencode_window or 'input' ---@cast focus 'input' | 'output'

lua/opencode/init.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ function M.setup(opts)
1010

1111
require('opencode.ui.highlight').setup()
1212
require('opencode.core').setup()
13-
require('opencode.quick_chat').setup()
1413
require('opencode.api').setup()
1514
require('opencode.keymap').setup(config.keymap)
1615
require('opencode.ui.completion').setup()

lua/opencode/quick_chat.lua

Lines changed: 62 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ local core = require('opencode.core')
55
local util = require('opencode.util')
66
local session = require('opencode.session')
77
local Promise = require('opencode.promise')
8-
local search_replace = require('opencode.quick_chat.search_replace')
98
local CursorSpinner = require('opencode.quick_chat.spinner')
109

1110
local M = {}
@@ -16,6 +15,7 @@ local M = {}
1615
---@field col integer Column position for spinner
1716
---@field spinner CursorSpinner Spinner instance
1817
---@field timestamp integer Timestamp when session started
18+
---@field range table|nil Range information
1919

2020
---@type table<string, OpencodeQuickChatRunningSession>
2121
local running_sessions = {}
@@ -130,21 +130,56 @@ end
130130
---@param message OpencodeMessage Message object
131131
---@return string response_text
132132
local function extract_response_text(message)
133+
if not message then
134+
return ''
135+
end
136+
133137
local response_text = ''
134138
for _, part in ipairs(message.parts or {}) do
135139
if part.type == 'text' and part.text then
136140
response_text = response_text .. part.text
137141
end
138142
end
139143

144+
-- Remove code blocks and inline code
145+
response_text = response_text:gsub('```[^`]*```', '')
146+
response_text = response_text:gsub('`[^`]*`', '')
147+
140148
return vim.trim(response_text)
141149
end
142150

151+
--- Applies raw code response to buffer (simple replacement)
152+
---@param buf integer Buffer handle
153+
---@param response_text string The raw code response
154+
---@param row integer Row position (0-indexed)
155+
---@param range table|nil Range information { start = number, stop = number }
156+
---@return boolean success Whether the replacement was successful
157+
local function apply_raw_code_response(buf, response_text, row, range)
158+
if response_text == '' then
159+
return false
160+
end
161+
162+
local lines = vim.split(response_text, '\n')
163+
164+
if range then
165+
-- Replace the selected range
166+
local start_line = range.start - 1 -- Convert to 0-indexed
167+
local end_line = range.stop - 1 -- Convert to 0-indexed
168+
vim.api.nvim_buf_set_lines(buf, start_line, end_line + 1, false, lines)
169+
else
170+
-- Replace current line
171+
vim.api.nvim_buf_set_lines(buf, row, row + 1, false, lines)
172+
end
173+
174+
return true
175+
end
176+
143177
--- Processes response from quickchat session
144178
---@param session_info table Session tracking info
145179
---@param messages OpencodeMessage[] Session messages
180+
---@param range table|nil Range information
146181
---@return boolean success Whether the response was processed successfully
147-
local function process_response(session_info, messages)
182+
local function process_response(session_info, messages, range)
148183
local response_message = messages[#messages]
149184
if #messages < 2 and (not response_message or response_message.info.role ~= 'assistant') then
150185
return false
@@ -157,39 +192,12 @@ local function process_response(session_info, messages)
157192
return false
158193
end
159194

160-
local replacements, parse_warnings = search_replace.parse_blocks(response_text)
161-
162-
-- Show parse warnings
163-
if #parse_warnings > 0 then
164-
for _, warning in ipairs(parse_warnings) do
165-
vim.notify('Quick chat: ' .. warning, vim.log.levels.WARN)
166-
end
167-
end
168-
169-
if #replacements == 0 then
170-
vim.notify('Quick chat: No valid SEARCH/REPLACE blocks found in response', vim.log.levels.WARN)
171-
return false
172-
end
173-
174-
local success, errors, applied_count = search_replace.apply(session_info.buf, replacements, session_info.row)
175-
176-
-- Provide detailed feedback
177-
if applied_count > 0 then
178-
local total_blocks = #replacements
179-
if applied_count == total_blocks then
180-
vim.notify(
181-
string.format('Quick chat: Applied %d change%s', applied_count, applied_count > 1 and 's' or ''),
182-
vim.log.levels.INFO
183-
)
184-
else
185-
vim.notify(string.format('Quick chat: Applied %d/%d changes', applied_count, total_blocks), vim.log.levels.INFO)
186-
end
187-
end
188-
189-
if #errors > 0 then
190-
for _, err in ipairs(errors) do
191-
vim.notify('Quick chat: ' .. err, vim.log.levels.WARN)
192-
end
195+
local success = apply_raw_code_response(session_info.buf, response_text, session_info.row, range)
196+
if success then
197+
local target = range and 'selection' or 'current line'
198+
vim.notify(string.format('Quick chat: Replaced %s with generated code', target), vim.log.levels.INFO)
199+
else
200+
vim.notify('Quick chat: Failed to apply raw code response', vim.log.levels.WARN)
193201
end
194202

195203
return success
@@ -213,7 +221,7 @@ local on_done = Promise.async(function(active_session)
213221
return
214222
end
215223

216-
local success = process_response(running_session, messages)
224+
local success = process_response(running_session, messages, running_session.range)
217225
if success then
218226
cleanup_session(running_session, active_session.id)
219227
else
@@ -262,77 +270,28 @@ local function create_context_config(has_range)
262270
}
263271
end
264272

265-
--- Generates instructions for the LLM to follow the SEARCH/REPLACE format
266-
--- This is inspired from Aider Chat approach
273+
--- Generates instructions for raw code generation mode
267274
---@param context_config OpencodeContextConfig Context configuration
268275
---@return string[] instructions Array of instruction lines
269-
local generate_search_replace_instructions = Promise.async(function(context_config)
270-
local base_instructions = {
271-
'You are a patch generation engine.',
272-
'TASK:',
273-
'Generate search/replace blocks to implement the requested change.',
274-
'',
275-
'OUTPUT FORMAT (MANDATORY):',
276-
'<<<<<<< SEARCH',
277-
'[exact original code]',
278-
'=======',
279-
'[modified code]',
280-
'>>>>>>> REPLACE',
281-
'',
282-
'RULES:',
283-
'- Output ONLY RAW patch blocks',
284-
'- Marker lines must match EXACTLY',
285-
'- Include 1-3 lines of context for unique matching',
286-
'- Only REPLACE may differ',
287-
'- Preserve whitespace',
288-
'- NEVER add explanations or extra text',
289-
'',
290-
'EXAMPLES (use ONLY as reference):',
291-
'Example 1 - Fix function:',
292-
'<<<<<<< SEARCH',
293-
'function hello() {',
294-
' console.log("hello")',
295-
'}',
296-
'=======',
297-
'function hello() {',
298-
' console.log("hello");',
299-
'}',
300-
'>>>>>>> REPLACE',
301-
'',
302-
'Example 2 - Insert at cursor:',
303-
'<<<<<<< SEARCH',
304-
'',
305-
'=======',
306-
'local new_variable = "value"',
307-
'>>>>>>> REPLACE',
308-
'',
309-
}
310-
311-
local context_guidance = {}
312-
313-
-- Check context configuration to determine guidance
314-
if context_config.diagnostics and context_config.diagnostics.enabled then
315-
table.insert(context_guidance, 'Fix [DIAGNOSTICS] only (if asked)')
316-
end
276+
local function generate_raw_code_instructions(context_config)
277+
local context_info = ''
317278

318-
if context_config.selection then
319-
table.insert(context_guidance, 'Modify only [SELECTED RANGE]')
279+
if context_config.selection and context_config.selection.enabled then
280+
context_info = 'You have been provided with a code selection [SELECTED CODE]. '
320281
elseif context_config.cursor_data and context_config.cursor_data.enabled then
321-
table.insert(context_guidance, 'Modify only [CURSOR POSITION]')
322-
end
323-
324-
if context_config.git_diff and context_config.git_diff.enabled then
325-
table.insert(context_guidance, "Use [GIT DIFF] only as reference (don't copy syntax)")
282+
context_info = 'You have been provided with cursor context. [CURSOR POSITION]'
326283
end
327284

328-
if #context_guidance > 0 then
329-
table.insert(base_instructions, 'CONTEXT GUIDANCE: ' .. table.concat(context_guidance, ', ') .. '.')
330-
end
331-
332-
table.insert(base_instructions, '')
285+
local buf = vim.api.nvim_get_current_buf()
286+
local filetype = vim.api.nvim_buf_get_option(buf, 'filetype')
333287

334-
return base_instructions
335-
end)
288+
return {
289+
'I want you to act as a senior ' .. filetype .. ' developer. ' .. context_info,
290+
'I will ask you specific questions and I want you to return raw code only ',
291+
'(no codeblocks, no explanations). ',
292+
"If you can't respond with code, respond with nothing.",
293+
}
294+
end
336295

337296
--- Creates message parameters for quick chat
338297
---@param message string The user message
@@ -350,7 +309,8 @@ local create_message = Promise.async(function(message, buf, range, context_confi
350309
end
351310

352311
local result = context.format_quick_chat_message(message, context_config, format_opts):await()
353-
local instructions = quick_chat_config.instructions or generate_search_replace_instructions(context_config):await()
312+
313+
local instructions = quick_chat_config.instructions or generate_raw_code_instructions(context_config)
354314

355315
local parts = {
356316
{ type = 'text', text = table.concat(instructions, '\n') },
@@ -421,6 +381,7 @@ M.quick_chat = Promise.async(function(message, options, range)
421381
col = col,
422382
spinner = spinner,
423383
timestamp = vim.uv.now(),
384+
range = range,
424385
}
425386

426387
-- Set up global keymaps for quick chat

0 commit comments

Comments
 (0)