Skip to content

split_rendering.rs Refactor Plan

Context

crates/fresh-editor/src/view/ui/split_rendering.rs is 8,635 lines in a single file. A single impl SplitRenderer block spans ~5,975 lines (L922–L6896) and an inline mod tests block adds another ~1,740 lines. Several methods are individually enormous:

MethodLinesNotes
render_view_lines~1,246Per-line character/span loop; the hot path
render_content~696Top-level entry: tabs, splits, hit areas
render_composite_buffer~436Composite (side-by-side) renderer
apply_wrapping_transform~323Hard-cap + soft wrap
compute_buffer_layout~282Layout phase for one buffer
compute_char_style~1997-layer style precedence
decoration_context~174Aggregates overlays/diagnostics/indicators

Goal

Break the file into cohesive, independent modules such that:

  1. The file becomes navigable (no file >~1,300 lines outside the irreducible inner loop).
  2. As many modules as possible are self-sustaining — they take typed, concern-scoped inputs and return typed outputs, with no reliance on a shared "mega struct".
  3. The remaining coupled code (the orchestration layer) is physically quarantined in its own subdirectory, so the coupling is visible at the filesystem level.

Where the coupling actually is

Most methods already take small, typed parameter lists. Shared-context coupling is concentrated in only five places:

  1. SplitRenderer — a zero-field unit struct used as a namespace for ~50 methods. Will be dissolved.
  2. LineRenderInput — 20+ fields. Used by exactly one function (render_view_lines). Will become private and split internally.
  3. LeftMarginContext — used by exactly one function (render_left_margin). Co-located and kept private.
  4. CharStyleContext / CharStyleOutput — used by exactly one function (compute_char_style). Co-located and kept private.
  5. SelectionContext / DecorationContext — the only genuinely shared carriers. Produced by two builder functions and consumed by the three big render functions. Quarantined into orchestration/.

Final directory layout

crates/fresh-editor/src/view/ui/split_rendering/
├── mod.rs                       re-exports the public API from orchestration/

│   ─── self-sustaining (no shared mega-structs) ───
├── spans.rs
├── style.rs
├── char_style.rs                private CharStyleContext / CharStyleOutput
├── base_tokens.rs
├── transforms.rs                wrap, soft_breaks, conceal, virtual_lines
├── view_data.rs
├── folding.rs                   FoldIndicator + fold/diff indicator builders
├── scrollbar.rs
├── layout.rs                    split layout, viewport sync, anchor, compose, separator
├── gutter.rs                    private LeftMarginContext
├── post_pass.rs                 osc8, hyperlinks, column guides, ruler bg, line bg

└── orchestration/               ─── shares SelectionContext / DecorationContext ───
    ├── mod.rs                   pub fn render_content, pub fn compute_content_layout
    ├── contexts.rs              SelectionContext, DecorationContext (data only)
    ├── overlays.rs              selection_context, decoration_context (producers)
    ├── render_line.rs           render_view_lines + private LineRenderInput split
    ├── render_buffer.rs         compute_buffer_layout, draw_buffer_in_split,
    │                            render_buffer_in_split, render_view_line_content
    └── render_composite.rs      render_composite_buffer
  • 11 self-sustaining files at the top level. None import the shared carriers.
  • 5 orchestration files plus orchestration/mod.rs. This is the only place that may import SelectionContext / DecorationContext.
  • Top-level mod.rs is a thin façade that re-exports the public API.

Visibility rules (enforced by convention + grep)

File setMay importMay NOT import
Top-level (11 self-sustaining files)stdlib, ratatui, crate primitives, ViewLine, Theme, Buffer, EditorState, overlays, etc.Never orchestration::*, never SelectionContext / DecorationContext
orchestration/*everything above + top-level split_rendering modules + contexts::*
mod.rsonly pub use orchestration::{render_content, compute_content_layout};anything else

The invariant is cheap to lock in: a single grep (grep -n 'SelectionContext\|DecorationContext' <top-level files>) should return zero hits outside orchestration/.

File-by-file content & size estimates

#FileApprox sizeContents
1mod.rs~60pub use re-exports; optional compat SplitRenderer shim
2spans.rs~300push_span_with_map, SpanAccumulator, span_color_at, span_info_at, compress_chars, compute_inline_diff, debug_tag_style, push_debug_tag, DebugSpanTracker
3style.rs~150dim_color_for_tilde, inline_diagnostic_style, fold_placeholder_style, append_fold_placeholder, create_virtual_line
4char_style.rs~250compute_char_style + private CharStyleContext / CharStyleOutput
5base_tokens.rs~320build_base_tokens, build_base_tokens_binary, build_base_tokens_for_hook, is_binary_unprintable, is_control_char
6transforms.rs~640apply_wrapping_transform, apply_soft_breaks, apply_conceal_ranges, inject_virtual_lines
7view_data.rs~280build_view_data, view_line_source_byte, is_hidden_byte
8folding.rs~260apply_folding, fold_adjusted_visible_count, fold_indicators_for_viewport, diff_indicators_for_viewport, FoldIndicator
9scrollbar.rs~500render_scrollbar, render_horizontal_scrollbar, render_composite_scrollbar, scrollbar_line_counts, scrollbar_visual_row_counts, compute_max_line_length
10layout.rs~290split_layout, split_buffers_for_tabs, sync_viewport_to_content, resolve_view_preferences, calculate_view_anchor, calculate_compose_layout, calculate_viewport_end, resolve_cursor_fallback, render_separator. Local types: SplitLayout, ViewPreferences, ViewAnchor, ComposeLayout
11gutter.rs~230render_left_margin + private LeftMarginContext, render_compose_margins
12post_pass.rs~240render_column_guides, render_ruler_bg, apply_hyperlink_overlays, apply_osc8_to_cells, apply_background_to_lines
13orchestration/mod.rs~800pub fn render_content, pub fn compute_content_layout
14orchestration/contexts.rs~30SelectionContext, DecorationContext — data only
15orchestration/overlays.rs~260selection_context, decoration_context
16orchestration/render_line.rs~1,300render_view_lines + private concern-scoped sub-structs replacing LineRenderInput; LineRenderOutput / LastLineEnd
17orchestration/render_buffer.rs~550compute_buffer_layout, draw_buffer_in_split, render_buffer_in_split, render_view_line_content; BufferLayoutOutput
18orchestration/render_composite.rs~440render_composite_buffer
19tests/~1,740cursor.rs, tokens_and_wrap.rs, post_pass.rs, folding_and_highlight.rs, shared helpers in tests/mod.rs

Mega-struct locality recap

StructNew locationUsed by
SelectionContextorchestration/contexts.rsorchestration/overlays.rs (producer), render_line.rs, render_buffer.rs, render_composite.rs
DecorationContextorchestration/contexts.rssame
LineRenderInput (split into MarginArgs, CursorArgs, DecorArgs, CellMapArgs)private inside orchestration/render_line.rsnowhere else
BufferLayoutOutput, LineRenderOutput, LastLineEndorchestration/render_buffer.rs / render_line.rs as private typesinternal only
CharStyleContext / CharStyleOutputprivate inside char_style.rsnowhere else
LeftMarginContextprivate inside gutter.rsnowhere else
FoldIndicatorfolding.rsreferenced as a field type by DecorationContext
SplitRendererdeleted (or 4-line compat shim in mod.rs)entry point only

Internal decomposition of render_view_lines

This function (~1,246 lines) cannot be moved cleanly without shrinking LineRenderInput. Inside orchestration/render_line.rs:

rust
// private to the file
struct MarginArgs<'a>  { /* state, theme, gutter_width, estimated_lines,
                           indicators, show_*, cursor_line_* */ }
struct CursorArgs<'a>  { /* session_mode, software_cursor_only, primary
                           cursor pos, is_active */ }
struct DecorArgs<'a>   { /* decorations, view_lines, selection */ }
struct CellMapArgs<'a> { /* cell_theme_map, screen_width */ }

The outer render_view_lines builds these from its single parameter list, then delegates to:

  • render_line_chars(...) — per-char inner loop (currently inlined).
  • append_inline_diagnostic(...) — trailing diagnostic text.
  • CellThemeRecorder — small helper mirroring DebugSpanTracker for cell-theme-map writes.
  • ANSI parser threading — its own small function.

Nothing escapes the file.

mod.rs final shape

rust
//! Split pane layout and buffer rendering.
mod spans;
mod style;
mod char_style;
mod base_tokens;
mod transforms;
mod view_data;
mod folding;
mod scrollbar;
mod layout;
mod gutter;
mod post_pass;
mod orchestration;

pub use orchestration::{compute_content_layout, render_content};

// Optional API-compat shim — only if external callers currently use
// `SplitRenderer::…`. Can be removed once call sites are updated.
pub struct SplitRenderer;
impl SplitRenderer {
    pub fn render_content(/* … */) -> /* … */ { orchestration::render_content(/* … */) }
    pub fn compute_content_layout(/* … */) -> /* … */ { orchestration::compute_content_layout(/* … */) }
}

Phased execution

Each phase is a standalone PR. Every phase except Phase 4 is a pure code move (no signature changes visible outside the module). Existing inline tests cover every phase up to Phase 5.

Phase 1 — Leaves

Move to top-level files, converting impl SplitRenderer methods to free pub(super) fn:

  • spans.rs
  • style.rs
  • char_style.rs (fold its private context struct in)
  • post_pass.rs

Phase 2 — View pipeline & subsystems

  • base_tokens.rs
  • transforms.rs
  • view_data.rs
  • folding.rs
  • scrollbar.rs
  • layout.rs (includes render_separator)
  • gutter.rs (fold in LeftMarginContext)

Phase 3 — Quarantine orchestration

  • Create orchestration/.
  • Move SelectionContext, DecorationContext to orchestration/contexts.rs.
  • Move their producers to orchestration/overlays.rs.
  • Move the three big render functions into orchestration/render_line.rs, render_buffer.rs, render_composite.rs. No structural changes yet.

Phase 4 — Decompose render_view_lines

The only phase that changes logic shape. Inside orchestration/render_line.rs:

  • Split LineRenderInput into the four concern-scoped private sub-structs.
  • Extract the per-char inner loop, inline-diagnostic trailing text, ANSI threading, and cell-theme recording into named private helpers.
  • Add targeted unit tests for cursor placement and cell-theme-map writes before refactoring.

Phase 5 — mod.rs and the compat shim

  • Shrink mod.rs to re-exports.
  • Decide whether to keep the SplitRenderer shim or delete it along with updates to external call sites (all inside fresh-editor).

Phase 6 — Tests

  • Split mod tests across tests/ submodules.
  • Move shared helpers (render_output_for, render_output_for_with_gutters, dump_render_output, count_all_cursors, check_typing_at_cursor, extract_token_offsets, strip_osc8, read_row) to tests/mod.rs.
  • Group tests by target module: cursor.rs, tokens_and_wrap.rs, post_pass.rs, folding_and_highlight.rs.

Risks & mitigations

  • render_view_lines shares local bookkeeping (ANSI parser state, cell theme cursor, secondary-cursor collector). Premature extraction can silently break cursor placement. Mitigation: cover with added unit tests before Phase 4.
  • compute_buffer_layout and render_composite_buffer share a lot of structure with slightly different fold/wrap assumptions. Merging them is tempting but should be deferred to a post-refactor follow-up.
  • LineRenderInput holds &mut cell_theme_map. Keep it pub(super) at the module boundary; never re-export from orchestration::mod.

Success criteria

  • crates/fresh-editor/src/view/ui/split_rendering.rs no longer exists; it is replaced by the directory above.
  • grep -rn 'SelectionContext\|DecorationContext' crates/fresh-editor/src/view/ui/split_rendering/ matches only files under orchestration/.
  • No file in the module is >1,400 lines (with the singular exception of orchestration/render_line.rs until Phase 4 completes).
  • All existing tests pass at each phase boundary.
  • Public API from the module is unchanged (SplitRenderer::render_content, SplitRenderer::compute_content_layout).

Released under the Apache 2.0 License