Skip to content

refactor(renderer): decouple events from buffer manipulation #340

Open
sudo-tee wants to merge 21 commits intomainfrom
refactor/renderer-components
Open

refactor(renderer): decouple events from buffer manipulation #340
sudo-tee wants to merge 21 commits intomainfrom
refactor/renderer-components

Conversation

@sudo-tee
Copy link
Copy Markdown
Owner

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RenderBlock component 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.

@sudo-tee sudo-tee force-pushed the refactor/renderer-components branch 2 times, most recently from f1bf0d7 to 228b6be Compare March 24, 2026 13:21
@sudo-tee sudo-tee changed the title refactor(renderer): decouple events frob buffer manipulation refactor(renderer): decouple events from buffer manipulation Mar 27, 2026
@sudo-tee sudo-tee force-pushed the refactor/renderer-components branch from eabd6a5 to dd6dba0 Compare March 27, 2026 20:27
@sudo-tee sudo-tee requested a review from Copilot March 27, 2026 20:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_action now computes display_line as (line or output:get_line_count()) - 2, which can produce -1 for common call sites like _format_patch() where format_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 of output:add_line(...)/format_action or keep the previous - 1 behavior).
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.

@sudo-tee sudo-tee force-pushed the refactor/renderer-components branch 2 times, most recently from a8e4579 to 7306c13 Compare April 1, 2026 12:56
sudo-tee and others added 18 commits April 1, 2026 10:35
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.
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.
sudo-tee added 2 commits April 1, 2026 10:38
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.
@sudo-tee sudo-tee force-pushed the refactor/renderer-components branch from 18f0e78 to 53d5864 Compare April 1, 2026 14:38
@sudo-tee
Copy link
Copy Markdown
Owner Author

sudo-tee commented Apr 1, 2026

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

@disrupted
Copy link
Copy Markdown
Contributor

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

@sudo-tee
Copy link
Copy Markdown
Owner Author

sudo-tee commented Apr 2, 2026

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.
@sudo-tee
Copy link
Copy Markdown
Owner Author

sudo-tee commented Apr 2, 2026

@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 :)

@jensenojs
Copy link
Copy Markdown
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants