docs/internal/refactoring-planning-prompt.md
This file is an LLM prompt. Give it (or paste it) to an LLM tasked with
producing a refactoring plan for some portion of this codebase. It encodes
the structure, principles, and rigour the project expects from a plan — the
shape established by
editor-modules-refactor-plan.md and
split-rendering-refactor-plan.md,
which are the canonical worked examples.
When adapting the output to a new target, read both reference plans first. They are the contract; this prompt is the scaffolding.
A single big struct with dozens of flat fields, touched by impl
blocks scattered across many files.
The symptom: impl Editor { … } appears in lsp_actions.rs,
popup_actions.rs, clipboard.rs, buffer_management.rs, and a dozen
more. It looks modular. It isn't: every one of those methods can read
and write any of Editor's ~67 fields. The files are partitioned;
the state is not. A unit test for clipboard still needs a full
Editor. Renaming a buffer field still ripples everywhere.
The refactor is fundamentally about grouping fields. Not about
moving impl blocks into new directories — that's cosmetic. The job is:
impl blocks across the codebase.impl-file implicitly "owns" a cluster (even though
it can touch all fields)? Those clusters are the latent subsystems.MacroState,
BookmarkState, LspState, ClipboardState) in its own file, with
impl blocks on that new struct.macros: Vec<Macro>, macro_recording: bool, last_macro_register: Option<char>, macro_playing: bool — it holds macros: MacroState.
It is now a composition of ~25 owned subsystems, not a flat bag of
~67 fields.impl Editor method to impl <SubStruct>
on the struct that owns the fields it touches. Only methods that
genuinely need to combine multiple subsystems stay as
impl <GodStruct> orchestrators.The god struct doesn't need to contain those fields directly — that's
the whole point. After the refactor, it contains sub-structs that
contain those fields. The consequence — the observable success criterion
— is that only one file contains impl <GodStruct>. But the mechanism
is the field clustering and composition, not the impl-move. A plan
that describes new directories without naming the field clusters being
extracted is missing the actual work.
If your plan ends with impl Editor blocks still scattered across many
files, or with the god struct still holding the same flat fields under
a new directory layout, your plan is wrong. Everything else in this
document serves this transformation.
You are planning a structural refactoring of a specific file, module, or
subsystem in the sinelaw/fresh Rust codebase (a terminal IDE/editor).
You are not making behavioural changes. You are not adding features.
The goal is to make the code easier to read, test, and evolve — without a
flag day, without leaving main broken at any commit, and without adding
speculative abstractions.
The deliverable is a single Markdown document living at
docs/internal/<target>-refactor-plan.md. A reader of that document should
be able to execute the refactor in PR-sized commits without asking you
follow-up questions.
Either:
app/mod.rs,
view/ui/split_rendering.rs, lsp/client.rs), orIf the scope is ambiguous, ask one clarifying question before planning. Prefer narrower scopes — a plan that covers one 8k-line file is more useful than a plan that covers "the editor".
A plan that can't cite numbers is a wishlist. Do the measuring yourself
using Grep, Glob, and Read. The measurements directly feed the
field-clustering work in §3 of your plan.
Field inventory on the god struct. Read the struct definition and list every field. Cite the total count. This is your universe — every field must end up in exactly one sub-struct (or stay on the god struct because it's genuinely shared).
Scattered-impl audit. Run
rg -l "impl <TargetType>\b" <target-dir> and list every file that
matches. In this codebase that is a long list, and that is exactly the
problem. Cite the count.
Field-access matrix (the core measurement). For each file in the
scattered-impl list, which fields of the god struct does it read or
write? Use Grep with patterns like self\.<field> per field per file.
You don't need a full matrix — you need to identify clusters:
macro_actions.rs → MacroState cluster.bookmark_actions.rs → BookmarkState.Method distribution. How many methods live in each scattered-impl
file? How many fields does each file's methods touch? A file whose
methods collectively touch >50% of the god struct's fields is doing
orchestration disguised as a concern.
Largest methods. A 1,000+ line handle_action, render, or
process_async_messages is its own category — call these out
separately, as they will need individual plans in §7.
Shared "mega-struct" contexts. Types with >10 fields that are
passed between functions (e.g. SelectionContext,
LineRenderInput). These are the same anti-pattern at a smaller
scale and should appear in the measurements table.
External call sites of the module's public API (so you know the blast radius of any signature changes).
Put the raw measurements in tables in §1 of your plan. The headline
tables are (a) the scattered-impl file list and (b) the proposed
field clusters. If you can't measure something cheaply, say so — don't
guess.
These are the same principles the two reference plans are built on. Apply them — and, where a principle is already well-stated in one of the reference plans, quote it by reference rather than re-deriving it.
impl file per god type (the load-bearing rule). Only one
file may contain impl <GodType>. Everywhere else, you own a
subsystem struct and put methods on that. This is not an aesthetic
preference — it is the keystone. Without it, every other rule gets
eroded the next time someone needs "just one quick field". Enforce it
with a grep audit in the success criteria.&mut self meaning the subsystem — never &mut Editor.self.b_field. Not Rc<RefCell<B>>. Not a
back-pointer. A function signature.handle_action-style match blocks
contain no logic; each arm calls one subsystem method.SelectionContext in the rendering plan), put the
files that touch it in their own subdirectory so the coupling is visible
from ls.The reference plans elevate some of these to "hard rules" with numbers (Rule 1, Rule 2, …). Do the same — pick the 3–6 rules that matter most for your target and name them. Numbered rules are load-bearing: they give reviewers something to point at.
Produce a document with these sections, in this order. Each section has a prescribed purpose; don't skip and don't add new top-level sections unless the target genuinely demands it.
A short paragraph naming the target file(s) and the problem, followed by a table of concrete measurements (as described above). If the target has a single mega-method or mega-struct, list its subparts with line counts. No prose without numbers in this section.
One to three paragraphs. What specifically makes the current code hard to work with? Options include:
impl blocks that look modular but aren't.Name the specific instances (with line numbers or method names). Avoid generic "it's big" diagnoses — a 5000-line file isn't automatically a problem; five different concerns fused in a 5000-line file is.
This is the heart of the refactor. List every proposed sub-struct,
and for each: which fields of the god struct it absorbs, which scattered
impl files today are its implicit home, and a one-line description of
its concern. Example row shape from the editor-modules plan:
| New sub-struct | God-struct fields absorbed | Current impl home | Concern |
| MacroState | macros, macro_recording, last_macro_register, macro_playing | macro_actions.rs | Macro record/replay |
| BookmarkState | bookmarks, active_custom_contexts | (scattered in mod.rs) | Bookmark navigation |
| LspState | lsp_config, lsp_servers, lsp_progress, … (25 fields) | lsp_*.rs files | Language-server lifecycle |
Every field on the current god struct must appear in exactly one row (or be explicitly called out as "remains on the god struct" with a reason). That exhaustiveness is what makes the plan real.
Show the before/after struct definitions side by side. Before: 67 flat fields. After: ~25 fields, each a sub-struct. The diff is the deliverable.
Pick 3–6 principles from the list above (or your target's equivalents) and
state them as numbered "Rule N" clauses. Make at least one rule a hard
invariant that can be mechanically checked (grep audit, file-size cap, etc.).
Rule 1 should always be the single-impl-file rule for the god type.
Show — in code — what the end state looks like. Minimum content:
tree-style).impl block, to
establish the pattern.If the plan doesn't show the target shape concretely enough that a contributor could start today, it's not detailed enough.
Enumerate the small, fixed set of patterns you will use to cross subsystem boundaries. The editor-modules plan names four: orchestrator with split borrows, read-only context bundle, effects returned by the caller, event bus. The split-rendering plan names one (quarantined shared carriers).
Name them, and don't add a fifth mid-refactor. Decision rules for "which mechanism for which case" go here.
A table (or tables) mapping "currently here" → "moves to". Every non-trivial piece of logic in the target must appear in a row. If you haven't surveyed the target well enough to fill this table, the plan isn't ready.
Example row shapes from the references:
| Currently in mod.rs | Moves to |
| `SearchScanState`, `LineScanState` | `app/search/scan.rs` and `app/buffers/line_scan.rs` |
Every refactor has 2–4 genuinely hard cases that a naive plan glosses over. Name them explicitly and describe how you'll handle each. Common categories:
&mut self splits need to destructure
Editor { ref mut a, ref mut b, .. }? Where will you need
Effect-returning methods instead of direct mutation?render, handle_action, process_async_messages)?
Plan each one individually — show what its final shape looks like.main between phases? (Usually: old methods become thin delegators
until the last phase deletes them.)One phase per PR-sized unit of work. Every phase must:
Canonical phase ordering (adapt as needed):
&self methods. Zero risk, establishes the
pattern, surfaces hidden dependencies early.handle_action is in
scope) — one commit per arm group.buffer_management.rs).*_actions.rs files, enforce
the impl audit, shrink mod.rs to re-exports.For each phase: list the exact steps, cite the risk, and name the test coverage you'll rely on (unit tests, visual-regression harness, etc.).
Measurable, mechanically-checkable criteria. Minimum:
impl audit. rg "impl <GodType>" across the target returns only
the single expected file. Non-negotiable.self\.<old_field_name> outside the owning module returns zero hits.A short list of "this could go wrong, here's what saves us". Local bookkeeping that's easy to silently break (cursor placement, ANSI parser state, undo boundaries) belongs here. If a risk has no mitigation beyond "be careful", say so — don't invent a mitigation.
foo.rs L100–L400
is worth ten paragraphs of "the large method in foo".Ask yourself these questions. If the answer to any of them is no, revise.
impl file touches
which fields?impl <GodType>" mechanically verifiable?If yes to all: submit the plan as
docs/internal/<target>-refactor-plan.md.
Read these in full before drafting your own. The structure, tone, and level of concreteness they exhibit are what this prompt asks for.
docs/internal/editor-modules-refactor-plan.md — four mega-files in
crates/fresh-editor/src/app/. Shows the god-object decomposition case
and the four-coordination-mechanisms framework.docs/internal/split-rendering-refactor-plan.md — one 8,635-line file.
Shows the quarantine strategy (physically segregate coupled code into a
subdirectory) and the leaf-modules-first phasing.