refactor(renderer): decouple events from buffer manipulation #340
refactor(renderer): decouple events from buffer manipulation #340
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the UI renderer to move away from event-driven buffer mutations toward a component/block-based rendering pipeline with caching, scheduling, and (optional) viewport virtualization.
Changes:
- Introduces a
RenderBlockcomponent model (message/part/overlay) with renderer-side caching and a backend applier (full + patch). - Adds virtualization utilities (tail budgets + viewport-based selection) and measurement helpers.
- Reworks renderer event handling to mark dirty state and schedule batched renders; expands unit coverage for the new modules.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
lua/opencode/ui/renderer.lua |
Coordinates full/session renders via session view → virtualization → backend apply; adds viewport-aware rendering. |
lua/opencode/ui/renderer/events.lua |
Moves event handlers toward state mutation + cache invalidation + scheduled renders. |
lua/opencode/ui/renderer/buffer.lua |
Adds block flattening + patch application and render-state rebuild from blocks. |
lua/opencode/ui/renderer/cache.lua |
New cache for message/part RenderBlocks and revision helpers. |
lua/opencode/ui/renderer/components/{message,part,overlays}.lua |
New component renderers producing RenderBlocks. |
lua/opencode/ui/renderer/{session_view,virtualize,viewport,measure}.lua |
New block composition + visibility selection utilities. |
lua/opencode/ui/renderer/{scheduler,backend,ctx}.lua |
New render batching scheduler, backend wrapper, and expanded renderer context. |
lua/opencode/ui/formatter.lua |
Adds explicit render context plumbing and new render_* APIs. |
lua/opencode/ui/formatter/tools/task.lua |
Adjusts action positioning for task tool output. |
lua/opencode/ui/ui.lua |
Wraps Treesitter language registration to avoid hard failures. |
lua/opencode/config.lua |
Adds viewport configuration defaults. |
tests/unit/renderer/* |
New unit test coverage for cache/components/viewport/virtualize/backend/scheduler/events. |
docs/virtualized-renderer-plan.md |
Migration plan documentation for the virtualized renderer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f1bf0d7 to
228b6be
Compare
eabd6a5 to
dd6dba0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
lua/opencode/ui/formatter.lua:112
add_actionnow computesdisplay_lineas(line or output:get_line_count()) - 2, which can produce-1for common call sites like_format_patch()whereformat_action()only adds a single line. That will anchor actions to an invalid/incorrect line. Compute the action line based on the actual header line index (e.g., use the return value ofoutput:add_line(...)/format_actionor keep the previous- 1behavior).
local function add_action(output, text, action_type, args, key, line)
-- actions use api-indexing (e.g. 0 indexed)
line = (line or output:get_line_count()) - 2
output:add_action({
text = text,
type = action_type,
args = args,
key = key,
display_line = line,
range = { from = line, to = line },
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
a8e4579 to
7306c13
Compare
Introduce a renderer.flush scheduler to batch dirty/removed message
…and extmark allocs - Wrap apply_pending and end_bulk_mode writes in eventignore='all' so render-markdown only fires from the explicit request_on_data_rendered call, not once per nvim_buf_set_lines during streaming or session load - set_lines noautocmd opts removed; bulk caller sets eventignore directly via begin_update/end_update, keeping the API clean - set_extmarks: skip deepcopy for marks with no start_col and no end_row offset needed (~99% of extmarks), eliminating 125k copies per session load - add_vertical_border: build extmark_opts once per call instead of per line - get_markdown_filetype: memoize vim.filetype.match results by filename - accumulate_bulk_extmarks helper deduplicates 3x copy-pasted extmark loop - Remove dead bulk_message_positions/bulk_part_positions tracking fields - Remove unused write_formatted_data export and dead append_part_now bulk branch - Hoist lines_equal/extmarks_equal out of format_message closure; replace vim.inspect comparison with vim.deep_equal
…nd simplify flush Remove unused ctx.prev_line_count from the renderer context and reset. Simplify M.flush() to only request data-rendered when changes were applied and the renderer is not in bulk_mode. Extract window-scrolling into M.scroll_win_to_bottom() and reuse it from post_flush(). tests: make replay deterministic and robust for bulk mode Sort extmarks deterministically in test helpers, clear/reset UI and renderer state during replay setup, and force synchronous completion (flush.end_bulk_mode()) in replay tests when ctx.bulk_mode is active so assertions run against final output.
- dialog: add support to extend vertical border to trailing blank via config.extend_border_to_trailing_blank
- renderer: wrap full-session rendering in bulk flush (begin/end) and delegate scroll-to-bottom to scroll module
- output_window: simplify visible-bottom-line calculation to use vim.fn.line('w$')
- ui: guard treesitter language registration with pcall to avoid errors in minimal builds
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Replace cursor-based viewport tracking with a persistent _was_at_bottom_by_win flag so output windows correctly decide whether to auto-scroll when the buffer grows. is_at_bottom() now prefers the sticky flag and falls back to a live visible-bottom check on first use. sync_cursor_with_viewport() updates the flag when the viewport moves away from the last line and scroll_win_to_bottom() sets the flag when explicitly scrolling to the end. Tests updated to exercise viewport-driven behavior and to simulate WinScrolled events. This makes tail-following robust against mouse/viewport scrolling where the cursor may not move.
…ernal messages, normalize extmarks - Remove extend_border_to_trailing_blank from permission and question window displays. - Filter messages with ids starting with __opencode_ before calling full-session render. - Normalize extmark serialization (move virt_text_pos, virt_text_hide, virt_text_win_col, priority into consistent ordering) and update expected test fixtures/timestamps. - Update unit tests to match new display options.
Add an ignore list for server events (e.g. "server.heartbeat") so noisy heartbeats don't trigger rendering. Also guard ThrottlingEmitter:_drain to skip calling the process function when there are no items to process, avoiding unnecessary work.
…update cleanup Add a with_suppressed_output_autocmds helper that temporarily sets eventignorewin on the output window, wraps output_window.begin_update / end_update with xpcall to guarantee cleanup, and restores the original state even if the wrapped operation errors. Use the helper in apply_pending and end_bulk_mode so writes and part/message updates won't trigger autocmds mid-write and cleanup always runs. Re-throw errors after restoring state. Add unit tests exercising failure during bulk writes to verify eventignore is restored, end_update is called, and bulk mode state is cleared.
Revert to simpler jobs_count logic
Normalize parts in resolve_grep_string to safely convert nil, vim.NIL, numbers, and booleans to strings and prefer input.path over input.include when present. This prevents invalid grep strings from non-string inputs.
…lears Extract extmark_clear_range to compute the range of extmarks to clear and return clear_start/clear_end. Change apply_extmarks to accept a skip_clear flag so callers can control when clearing happens. Clear extmarks once before writing lines in upsert_message_now and upsert_part_now, and during bulk flush. Add unit tests to assert extmarks are cleared before replay and that clear_extmarks is called before set_lines.
Adjust indexing used when anchoring actions and task ranges so actions
and extmarks align with their rendered lines:
- formatter.add_action: use (line or output:get_line_count()) - 1
- formatter/tools/task: set display_line = start_line and range = { from = start_line + 1, to = end_line + 1 }
- renderer/buffer: pass line_start to add_actions instead of line_start + 1
- renderer: avoid inserting synthetic revert message into state.messages; notify via events only
Add tests:
- unit tests for formatter anchoring and assistant-mode fallback
- replay test asserting a single synthetic revert message is produced
Fixes off-by-one rendering/anchor issues and adds tests to prevent regressions.
Use pcall to probe for support of the 'eventignorewin' option and only set/restore it when available. Prevents runtime errors on older Neovim versions (e.g. in CI) while keeping the output window update logic intact.
Add Lua annotations (@param, @return) and short descriptive comments across many UI and renderer modules (base_picker, formatter, grep tool, highlight, output_window, question_window, renderer/buffer, renderer/ctx, renderer/events, renderer/flush, ui). These changes improve readability and editor tooling support. No behavioral changes.
…sions Probe 'eventignorewin' with pcall and record presence in has_eventignorewin. Only restore and assert the window-local option when it exists to avoid failures on Neovim builds that don't expose 'eventignorewin'. Add explanatory comments and a small refactor in tests/unit/output_window_spec.lua.
18f0e78 to
53d5864
Compare
|
Ok Now I have something that is stable. The gist of this refactoring is to decouple the event system from buffer ops, so now on server events we update the render state and mark it dirty for the scheduler which will flush any dirty message/parts. This will become important for future optimizations as we can tweak the scheduler. This was a big refactoring, however I need some help ensuring it runs smoothly for everyone @jensenojs @disrupted if you can run this branch for a day or something to let me know if you find major issues it would be greatly appreciated. Thanks |
|
right away I've noticed that permission requests don't render reliably on this branch. or sometimes new messages arrive and the permission request is pushed up the message stack which makes it more difficult to spot pending requests |
Thanks for having a look, I will look at the permissions and questions more closely, |
Add pinned_bottom_message_ids and is_pinned_bottom_message to the buffer renderer so permission-display-message and question-display-message are kept pinned below later messages when rendering. Update get_message_insert_line logic to respect pinned messages and to prefer rendered pinned positions when available.
|
@disrupted I did a fix for this specific issue. Let me know if this happens again, If so try to provide me a replay session so I can reproduce. Thanks a lot :) |
|
Messages may be missed, and some sessions do not load at all when opened. I will take a closer look later I am using this recipe https://github.com/sudo-tee/opencode.nvim/blob/main/docs/recipes/bidirectional-sync/README.md |
No description provided.