docs/internal/editor-state-refactoring.md
Completed: ComposeState extraction (7 fields → sub-struct).
Remaining: EditorState still has 18 fields (17 + 1 sub-struct). Three more sub-structs would bring it down to ~6 top-level fields.
EditorState
├── buffer: Buffer ← core
├── cursors: Cursors ← core
├── primary_cursor_line_number: LineNumber ← core (cursor cache)
├── mode: String ← buffer flags
├── show_cursors: bool ← buffer flags
├── editing_disabled: bool ← buffer flags
├── highlighter: HighlightEngine ← highlighting
├── reference_highlighter: ReferenceHighlighter ← highlighting
├── reference_highlight_overlay: ReferenceHighlightOverlay ← highlighting
├── bracket_highlight_overlay: BracketHighlightOverlay ← highlighting
├── semantic_tokens: Option<SemanticTokenStore> ← highlighting
├── indent_calculator: RefCell<IndentCalculator> ← highlighting (language-derived)
├── overlays: OverlayManager ← decorations
├── marker_list: MarkerList ← decorations
├── virtual_texts: VirtualTextManager ← decorations
├── popups: PopupManager ← decorations
├── margins: MarginManager ← decorations
├── text_properties: TextPropertyManager ← decorations
├── buffer_settings: BufferSettings ← already extracted
├── compose: ComposeState ← already extracted
├── language: String ← metadata
└── (no more flat fields)
DecorationState — visual annotationspub struct DecorationState {
pub overlays: OverlayManager,
pub marker_list: MarkerList,
pub virtual_texts: VirtualTextManager,
pub popups: PopupManager,
pub margins: MarginManager,
pub text_properties: TextPropertyManager,
}
Why these belong together:
marker_list is the shared position-tracking substrate: overlays, virtual texts,
and margins all register markers and adjust together on insert/delete.popups are anchored to buffer positions and dismissed on focus loss.text_properties store metadata on text ranges for virtual buffers.Coupling to watch for:
apply_insert/apply_delete call marker_list.adjust_* and
margins.adjust_* — these must remain callable from EditorState's apply.
Expose decorations.adjust_for_insert(pos, len) /
decorations.adjust_for_delete(pos, len) convenience methods.overlays.add() + marker_list — callers
will need state.decorations.overlays / state.decorations.marker_list.Estimated touch count: ~40 call sites across state.rs, plugin_commands.rs,
split_rendering.rs, input.rs, and the Event::Add*/Event::Remove* arms.
HighlightState — syntax/semantic highlightingpub struct HighlightState {
pub engine: HighlightEngine,
pub indent_calculator: RefCell<IndentCalculator>,
pub reference_highlighter: ReferenceHighlighter,
pub reference_highlight_overlay: ReferenceHighlightOverlay,
pub bracket_highlight_overlay: BracketHighlightOverlay,
pub semantic_tokens: Option<SemanticTokenStore>,
}
Why these belong together:
engine does syntax highlighting,
indent_calculator needs grammar info, reference_highlighter uses language
to find word boundaries, and semantic_tokens come from the LSP for that language.reference_highlight_overlay and bracket_highlight_overlay are caches that
debounce re-computation of reference/bracket highlights — closely tied to the
highlighter.set_language_from_name), all of these
need resetting.Coupling to watch for:
apply_insert/apply_delete call highlighter.invalidate_range() — expose
via highlights.invalidate_range(range).highlighter + semantic_tokens together in
split_rendering.rs.indent_calculator uses RefCell for interior mutability; putting it inside the
sub-struct doesn't change borrow semantics since it's already behind RefCell.Estimated touch count: ~25 call sites across state.rs, split_rendering.rs,
file_operations.rs, lsp_requests.rs, prompt_actions.rs.
BufferFlags — access control flagspub struct BufferFlags {
pub mode: String,
pub show_cursors: bool,
pub editing_disabled: bool,
}
Why these belong together:
editing_disabled = true; show_cursors = false; mode = "special").This is the smallest extraction (~10 call sites) and could be done first as a quick win, or skipped if the overhead isn't justified for 3 fields.
EditorState
├── buffer: Buffer
├── cursors: Cursors
├── primary_cursor_line_number: LineNumber
├── buffer_settings: BufferSettings
├── compose: ComposeState
├── decorations: DecorationState
├── highlights: HighlightState
├── flags: BufferFlags
├── language: String
9 top-level fields (5 sub-structs + 4 core fields). Each sub-struct groups fields by concern and access pattern.
DecorationState — highest field count (6), clearest grouping, biggest
reduction. Do this first.HighlightState — second highest (6 fields), well-bounded access pattern.BufferFlags — smallest (3 fields), optional. Only worth doing if the
consistency of "everything is grouped" outweighs the churn.Each extraction is independent and can be done in a single commit following the
same pattern as the ComposeState extraction:
state.field → state.sub.field referencescargo build verifies completeness (all renamings are compile errors if missed)SplitViewState: some fields (e.g., margins,
debug_highlight_mode) could arguably be per-split rather than per-buffer.
That's a semantic change, not just a grouping refactor — punt to a separate
effort.SplitViewState: it has its own compose fields that mirror
ComposeState. Deduplication there (e.g., having splits hold an
Option<ComposeState> override) is a separate task.