Skip to content

Commit 2f73a21

Browse files
sudo-teejensenojs
andauthored
refactor(renderer): decouple events from buffer manipulation (#340)
Introduce a renderer.flush scheduler to batch dirty/removed message --- Co-authored-by: Jensen <[email protected]>
1 parent ed0c078 commit 2f73a21

57 files changed

Lines changed: 8722 additions & 2789 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lua/opencode/config.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ M.defaults = {
142142
rendering = {
143143
markdown_debounce_ms = 250,
144144
on_data_rendered = nil,
145+
markdown_on_idle = false,
146+
-- If set to a number, markdown rendering will be deferred while
147+
-- `state.user_message_count[session_id]` is greater than this value.
148+
-- If `nil`, the existing behavior is used (defer while > 0).
149+
markdown_on_idle_threshold = nil,
145150
event_throttle_ms = 40,
146151
event_collapsing = true,
147152
},
@@ -255,6 +260,8 @@ M.defaults = {
255260
enabled = false,
256261
capture_streamed_events = false,
257262
show_ids = true,
263+
highlight_changed_lines = false,
264+
highlight_changed_lines_timeout_ms = 120,
258265
quick_chat = {
259266
keep_session = false,
260267
set_active_session = false,

lua/opencode/core.lua

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -528,15 +528,30 @@ M.ensure_current_mode = Promise.async(function()
528528
return true
529529
end)
530530

531-
---Initialize current model if it's not already set.
532-
---@return string|nil The current model (or the default model, if configured)
531+
---Initialize current model from messages or config.
532+
---@return string|nil The current model
533533
M.initialize_current_model = Promise.async(function()
534+
if state.messages then
535+
for i = #state.messages, 1, -1 do
536+
local msg = state.messages[i]
537+
if msg and msg.info and msg.info.modelID and msg.info.providerID then
538+
local model_str = msg.info.providerID .. '/' .. msg.info.modelID
539+
if state.current_model ~= model_str then
540+
state.model.set_model(model_str)
541+
end
542+
if msg.info.mode and state.current_mode ~= msg.info.mode then
543+
state.model.set_mode(msg.info.mode)
544+
end
545+
return state.current_model
546+
end
547+
end
548+
end
549+
534550
if state.current_model then
535551
return state.current_model
536552
end
537553

538554
local cfg = require('opencode.config_file').get_opencode_config():await()
539-
540555
if cfg and cfg.model and cfg.model ~= '' then
541556
state.model.set_model(cfg.model)
542557
end
@@ -545,6 +560,8 @@ M.initialize_current_model = Promise.async(function()
545560
end)
546561

547562
M._on_user_message_count_change = Promise.async(function(_, new, old)
563+
require('opencode.ui.renderer.flush').flush_pending_on_data_rendered()
564+
548565
if config.hooks and config.hooks.on_done_thinking then
549566
local all_sessions = session.get_all_workspace_sessions():await()
550567
local done_sessions = vim.tbl_filter(function(s)

lua/opencode/id.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,3 @@ function M.get_prefixes()
142142
end
143143

144144
return M
145-

lua/opencode/throttling_emitter.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ function ThrottlingEmitter:_drain()
4646
local items_to_process = self.queue
4747
self.queue = {}
4848

49-
self.process_fn(items_to_process)
49+
if #items_to_process > 0 then
50+
self.process_fn(items_to_process)
51+
end
5052

5153
-- double check that items weren't added while processing
5254
if #self.queue > 0 and not self.drain_scheduled then

lua/opencode/types.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
---@class OpencodeUIOutputRenderingConfig
173173
---@field markdown_debounce_ms number
174174
---@field on_data_rendered (fun(buf: integer, win: integer)|boolean)|nil
175+
---@field markdown_on_idle boolean
175176
---@field event_throttle_ms number
176177
---@field event_collapsing boolean
177178

@@ -207,6 +208,8 @@
207208
---@field enabled boolean
208209
---@field capture_streamed_events boolean
209210
---@field show_ids boolean
211+
---@field highlight_changed_lines boolean
212+
---@field highlight_changed_lines_timeout_ms number
210213
---@field quick_chat {keep_session: boolean, set_active_session: boolean}
211214

212215
---@class OpencodeHooks

lua/opencode/ui/base_picker.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ local function telescope_ui(opts)
8585
local entry_display = require('telescope.pickers.entry_display')
8686

8787
-- Create displayer dynamically based on number of parts
88+
---@param picker_item PickerItem
89+
---@return table
8890
local function create_displayer(picker_item)
8991
local items = {}
9092
for _ in ipairs(picker_item.parts) do
@@ -129,6 +131,7 @@ local function telescope_ui(opts)
129131
return entry
130132
end
131133

134+
---@return unknown
132135
local function refresh_picker()
133136
return current_picker
134137
and current_picker:refresh(
@@ -232,6 +235,7 @@ end
232235
local function fzf_ui(opts)
233236
local fzf_lua = require('fzf-lua')
234237

238+
---@return table
235239
local function create_fzf_config()
236240
local has_multi_action = util.some(opts.actions, function(action)
237241
return action.multi_selection
@@ -261,6 +265,7 @@ local function fzf_ui(opts)
261265
}
262266
end
263267

268+
---@return fun(fzf_cb: fun(line?: string))
264269
local function create_finder()
265270
return function(fzf_cb)
266271
for idx, item in ipairs(opts.items) do
@@ -299,6 +304,7 @@ local function fzf_ui(opts)
299304
end
300305
end
301306

307+
---Reopen fzf-lua to reflect updated picker items.
302308
local function refresh_fzf()
303309
vim.schedule(function()
304310
fzf_ui(opts)
@@ -644,6 +650,7 @@ function M.create_picker_item(parts)
644650
parts = parts,
645651
}
646652

653+
---@return string
647654
function item:to_string()
648655
local texts = {}
649656
for _, part in ipairs(self.parts) do
@@ -652,6 +659,7 @@ function M.create_picker_item(parts)
652659
return table.concat(texts, ' ')
653660
end
654661

662+
---@return table
655663
function item:to_formatted_text()
656664
local formatted = {}
657665
for _, part in ipairs(self.parts) do

lua/opencode/ui/dialog.lua

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,11 @@ function Dialog:format_dialog(output, config)
261261
local end_line = output:get_line_count()
262262

263263
if config.border_hl then
264-
formatter.add_vertical_border(output, start_line + 1, end_line, config.border_hl, -2)
264+
local border_end = end_line
265+
if config.extend_border_to_trailing_blank then
266+
border_end = border_end + 1
267+
end
268+
formatter.add_vertical_border(output, start_line + 1, border_end, config.border_hl, -2)
265269
end
266270

267271
output:add_line('')
@@ -277,15 +281,22 @@ function Dialog:format_options(output, options)
277281
label = label .. ' - ' .. option.description
278282
end
279283

280-
local line_idx = output:get_line_count()
281284
local is_selected = self._selected_index == i
282285
local line_text = is_selected and string.format(' %d. %s ', i, label) or string.format(' %d. %s', i, label)
283286

284-
output:add_line(line_text)
287+
-- Output uses 0-based indexing for extmarks. The correct target for
288+
-- extmarks is the previous line count (0-based) because add_line will
289+
-- append a new line and increase the 1-based line count. Capture the
290+
-- current count first and then add the line so we can use that 0-based
291+
-- index for extmarks.
292+
-- add_line returns a 1-based line index; Output extmarks use 0-based
293+
-- keys, so subtract 1 to get the correct extmark key.
294+
local added_idx = output:add_line(line_text)
285295

286296
if is_selected then
287-
output:add_extmark(line_idx, { line_hl_group = 'OpencodeDialogOptionHover' } --[[@as OutputExtmark]])
288-
output:add_extmark(line_idx, {
297+
local extmark_idx = added_idx - 1
298+
output:add_extmark(extmark_idx, { line_hl_group = 'OpencodeDialogOptionHover' } --[[@as OutputExtmark]])
299+
output:add_extmark(extmark_idx, {
289300
start_col = 2,
290301
virt_text = { { '', 'OpencodeDialogOptionHover' } },
291302
virt_text_pos = 'overlay',
@@ -309,11 +320,16 @@ function Dialog:_setup_keymaps()
309320
if keymaps.up then
310321
for _, key in ipairs(keymaps.up) do
311322
if key and key ~= '' then
312-
vim.keymap.set('n', key, function()
313-
self:navigate(-1)
314-
end, vim.tbl_extend('force', keymap_opts, {
315-
desc = 'Dialog: navigate up',
316-
}))
323+
vim.keymap.set(
324+
'n',
325+
key,
326+
function()
327+
self:navigate(-1)
328+
end,
329+
vim.tbl_extend('force', keymap_opts, {
330+
desc = 'Dialog: navigate up',
331+
})
332+
)
317333
table.insert(self._keymaps, key)
318334
end
319335
end
@@ -322,31 +338,46 @@ function Dialog:_setup_keymaps()
322338
if keymaps.down then
323339
for _, key in ipairs(keymaps.down) do
324340
if key and key ~= '' then
325-
vim.keymap.set('n', key, function()
326-
self:navigate(1)
327-
end, vim.tbl_extend('force', keymap_opts, {
328-
desc = 'Dialog: navigate down',
329-
}))
341+
vim.keymap.set(
342+
'n',
343+
key,
344+
function()
345+
self:navigate(1)
346+
end,
347+
vim.tbl_extend('force', keymap_opts, {
348+
desc = 'Dialog: navigate down',
349+
})
350+
)
330351
table.insert(self._keymaps, key)
331352
end
332353
end
333354
end
334355

335356
if keymaps.select and keymaps.select ~= '' then
336-
vim.keymap.set('n', keymaps.select, function()
337-
self:select()
338-
end, vim.tbl_extend('force', keymap_opts, {
339-
desc = 'Dialog: select option',
340-
}))
357+
vim.keymap.set(
358+
'n',
359+
keymaps.select,
360+
function()
361+
self:select()
362+
end,
363+
vim.tbl_extend('force', keymap_opts, {
364+
desc = 'Dialog: select option',
365+
})
366+
)
341367
table.insert(self._keymaps, keymaps.select)
342368
end
343369

344370
if keymaps.dismiss and keymaps.dismiss ~= '' then
345-
vim.keymap.set('n', keymaps.dismiss, function()
346-
self:dismiss()
347-
end, vim.tbl_extend('force', keymap_opts, {
348-
desc = 'Dialog: dismiss',
349-
}))
371+
vim.keymap.set(
372+
'n',
373+
keymaps.dismiss,
374+
function()
375+
self:dismiss()
376+
end,
377+
vim.tbl_extend('force', keymap_opts, {
378+
desc = 'Dialog: dismiss',
379+
})
380+
)
350381
table.insert(self._keymaps, keymaps.dismiss)
351382
end
352383

@@ -355,15 +386,20 @@ function Dialog:_setup_keymaps()
355386
local number_keymap_opts = vim.tbl_extend('force', keymap_opts, { nowait = true })
356387
for i = 1, math.min(option_count, 9) do
357388
local key = tostring(i)
358-
vim.keymap.set('n', key, function()
359-
if not self._active or not self._config.check_focused() then
360-
return
361-
end
362-
self._selected_index = i
363-
self._config.on_select(i)
364-
end, vim.tbl_extend('force', number_keymap_opts, {
365-
desc = 'Dialog: select option ' .. key,
366-
}))
389+
vim.keymap.set(
390+
'n',
391+
key,
392+
function()
393+
if not self._active or not self._config.check_focused() then
394+
return
395+
end
396+
self._selected_index = i
397+
self._config.on_select(i)
398+
end,
399+
vim.tbl_extend('force', number_keymap_opts, {
400+
desc = 'Dialog: select option ' .. key,
401+
})
402+
)
367403
table.insert(self._keymaps, key)
368404
end
369405
end

0 commit comments

Comments
 (0)