Skip to content

Commit bdfd9d0

Browse files
committed
refactor(render): make lazy-render always active, fix cursor and gg
- Remove opts.lazy: lazy-render is always active as a perf optimization - Decouple hidden_count from lazy-render truncation (only max_messages counts) - Fix cursor jump after load_more: preserve position via anchor message - Add gg keymap: load all messages before jumping to top, so full history is searchable
1 parent 4417636 commit bdfd9d0

5 files changed

Lines changed: 206 additions & 83 deletions

File tree

lua/opencode/ui/output_window.lua

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,15 @@ end
593593
function M.setup_keymaps(windows)
594594
local keymap = require('opencode.keymap')
595595
keymap.setup_window_keymaps(config.keymap.output_window, windows.output_buf)
596+
597+
-- When lazy-render is active, gg only reaches the top of rendered content.
598+
-- Load all messages first so gg reaches the true start of history.
599+
-- See tests/unit/renderer_lazy_spec.lua: "gg loads all messages and makes them searchable"
600+
vim.keymap.set('n', 'gg', function()
601+
local renderer = require('opencode.ui.renderer')
602+
renderer.load_all_messages()
603+
return 'gg'
604+
end, { buffer = windows.output_buf, expr = true, remap = true })
596605
end
597606

598607
---@param windows OpencodeWindowState
@@ -633,13 +642,42 @@ function M.setup_autocmds(windows, group)
633642
end,
634643
})
635644

636-
-- Lazy-render: load more messages on scroll-to-top (debounced)
637-
local debounced_load_more = require('opencode.util').debounce(function()
638-
local renderer = require('opencode.ui.renderer')
639-
if renderer.load_more_messages() then
640-
pcall(vim.api.nvim_win_set_cursor, windows.output_win, { 3, 0 })
641-
end
642-
end, 150)
645+
-- Lazy-render: load more messages on scroll-to-top (debounced)
646+
local debounced_load_more = require('opencode.util').debounce(function()
647+
local renderer = require('opencode.ui.renderer')
648+
local render_state = require('opencode.ui.renderer.ctx').render_state
649+
-- Save the message at the top of the viewport before loading more
650+
local ok, cursor = pcall(vim.api.nvim_win_get_cursor, windows.output_win)
651+
local anchor_msg_id = nil
652+
local anchor_was_at_line = nil
653+
if ok and cursor then
654+
local top_line = cursor[1]
655+
for _, msg in ipairs(state.messages or {}) do
656+
local msg_id = msg.info and msg.info.id or ''
657+
if not msg_id:match('^__opencode_') then
658+
local rendered = render_state:get_message(msg_id)
659+
if rendered and rendered.line_start and rendered.line_start >= top_line then
660+
anchor_msg_id = msg_id
661+
anchor_was_at_line = rendered.line_start
662+
break
663+
end
664+
end
665+
end
666+
end
667+
668+
if renderer.load_more_messages() then
669+
-- Restore cursor to the anchor message's new position
670+
if anchor_msg_id then
671+
local rendered = render_state:get_message(anchor_msg_id)
672+
if rendered and rendered.line_start then
673+
pcall(vim.api.nvim_win_set_cursor, windows.output_win, { rendered.line_start, 0 })
674+
return
675+
end
676+
end
677+
-- Fallback: move to top of buffer
678+
pcall(vim.api.nvim_win_set_cursor, windows.output_win, { 1, 0 })
679+
end
680+
end, 150)
643681
vim.api.nvim_create_autocmd('WinScrolled', {
644682
group = group,
645683
buffer = windows.output_buf,

lua/opencode/ui/renderer.lua

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -349,20 +349,16 @@ function M._render_full_session_data(session_data, opts)
349349
local visible_messages, hidden_count = get_visible_session_messages(state.messages)
350350
local revert_index = get_revert_index(state.messages)
351351

352-
-- Lazy-render: only render viewport-sized message count.
353-
-- Active when ctx.lazy_render_count is set or opts.lazy=true.
354-
if lazy_limit == nil and opts.lazy then
355-
local initial = get_initial_render_count()
356-
if #visible_messages > initial then
357-
lazy_limit = initial
352+
if lazy_limit == nil then
353+
local initial = get_initial_render_count()
354+
if #visible_messages > initial then
355+
lazy_limit = initial
356+
end
357+
end
358+
ctx.lazy_render_count = lazy_limit
359+
if lazy_limit and #visible_messages > lazy_limit then
360+
visible_messages = vim.list_slice(visible_messages, #visible_messages - lazy_limit + 1)
358361
end
359-
end
360-
-- Persist back to ctx so it survives the next M.reset()
361-
ctx.lazy_render_count = lazy_limit
362-
if lazy_limit and #visible_messages > lazy_limit then
363-
hidden_count = hidden_count + (#visible_messages - lazy_limit)
364-
visible_messages = vim.list_slice(visible_messages, #visible_messages - lazy_limit + 1)
365-
end
366362

367363
local t_format_start = vim.uv.hrtime()
368364
flush.begin_bulk_mode()
@@ -435,10 +431,9 @@ function M.render_from_cache(session_data)
435431
if not output_window.mounted() or not state.api_client then
436432
return
437433
end
438-
M._render_full_session_data(session_data, {
439-
restore_model_from_messages = true,
440-
lazy = ctx.lazy_render_count ~= nil,
441-
})
434+
M._render_full_session_data(session_data, {
435+
restore_model_from_messages = true,
436+
})
442437
local active_session = state.active_session
443438
if active_session and active_session.id then
444439
require('opencode.ui.question_window').restore_pending_question(active_session.id)
@@ -468,17 +463,38 @@ function M.load_more_messages()
468463
return true
469464
end
470465

466+
---Load all remaining messages and re-render.
467+
---Used when user explicitly navigates to the top (gg) to ensure
468+
---the full history is available for navigation and search.
469+
---@return boolean Whether any messages were loaded
470+
function M.load_all_messages()
471+
if not state.messages then
472+
return false
473+
end
474+
local total = #get_visible_session_messages(state.messages)
475+
if total == 0 then
476+
return false
477+
end
478+
local current = ctx.lazy_render_count or 0
479+
if current >= total then
480+
return false
481+
end
482+
483+
ctx.lazy_render_count = total
484+
M.render_from_cache(state.messages)
485+
return true
486+
end
487+
471488
---Fetch the active session from the server and render it
472489
---@return Promise<OpencodeMessage[]>
473490
function M.render_full_session()
474491
if not output_window.mounted() or not state.api_client then
475492
return Promise.new():resolve(nil)
476493
end
477494
return fetch_session():and_then(function(session_data)
478-
M._render_full_session_data(session_data, {
479-
restore_model_from_messages = true,
480-
lazy = true,
481-
})
495+
M._render_full_session_data(session_data, {
496+
restore_model_from_messages = true,
497+
})
482498
local active_session = state.active_session
483499
if active_session and active_session.id then
484500
require('opencode.ui.question_window').restore_pending_question(active_session.id)

lua/opencode/ui/renderer/ctx.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ function ctx:reset()
5858
self.markdown_render_scheduled = false
5959
self.global_folds = {}
6060
self.part_folds = {}
61-
self.lazy_render_count = nil
6261
self:bulk_reset()
6362
end
6463

tests/helpers.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ function M.replay_setup()
2525
end
2626

2727
renderer.reset()
28+
-- Ensure replay tests render all messages (lazy-render is always active)
29+
require('opencode.ui.renderer.ctx').lazy_render_count = math.huge
2830
permission_window.clear_all()
2931
question_window._clear_dialog()
3032
question_window._current_question = nil

0 commit comments

Comments
 (0)