Skip to main content

strop_engine/editor/trace/
drive.rs

1//! The deterministic driver (R11): live drivers record one `Action` before
2//! the reducer or render work runs; replay consumes those actions and
3//! re-runs the SAME production handlers with native launches suppressed by
4//! the tape. Service interleaving is the consumption order at the single
5//! delivery boundary — never worker-completion wall time.
6use std::io;
7
8use serde::{Deserialize, Serialize};
9
10use crate::editor::events::AppEvent;
11use crate::editor::Editor;
12use strop_trace::replay::Tick;
13
14#[derive(Serialize, Deserialize)]
15pub struct StartupOpen {
16    pub target: crate::files::FileTarget,
17    pub intent: super::super::io::OpenIntent,
18}
19
20/// Every external step a replay reproduces. `Event` carries the shared
21/// typed `AppEvent` (terminal input, paste, resize, quit intent, and every
22/// completed worker/service result), so the live handler and the replayed
23/// handler are one function.
24#[derive(Serialize, Deserialize)]
25pub enum Action {
26    /// The explicit start-services step after the seed, with an optional owned
27    /// resource open. Local directories and remote resources share this path.
28    Start {
29        #[serde(default)]
30        open: Option<StartupOpen>,
31    },
32    /// One external delivery, recorded at handler entry — before stale
33    /// filters or acceptance decisions, so rejected results replay too.
34    Event(AppEvent),
35    /// One rendered frame: the dimensions are inputs, the canonical cell
36    /// observation is checked inside the shared draw.
37    Frame { columns: u16, rows: u16 },
38    /// Deliberate shutdown: session persistence is requested here and its
39    /// terminal publication must land as an Event before the tape's `End`.
40    Finish,
41}
42
43impl Editor {
44    /// Live drivers: record the action, then run the shared body.
45    pub fn recorded_action(&mut self, action: Action, tick: Tick) -> io::Result<()> {
46        self.tape.action(tick, &action)?;
47        self.apply_recorded(action)
48    }
49
50    /// The one body both modes execute. Native side effects are confined
51    /// to the tape-gated call sites inside these handlers.
52    pub(crate) fn apply_recorded(&mut self, action: Action) -> io::Result<()> {
53        let frame = matches!(action, Action::Frame { .. });
54        match action {
55            Action::Start { open } => {
56                self.resolution.enabled = true;
57                let startup_message = self.message.clone();
58                if let Some(open) = open {
59                    self.lsp_start_services();
60                    self.request_target(open.target, open.intent);
61                } else {
62                    self.discover_git();
63                    self.lsp_start_services();
64                }
65                if !startup_message.is_empty() {
66                    self.message = startup_message;
67                }
68            }
69            Action::Event(event) => {
70                self.handle_app_event(event);
71                if !self.docs.is_empty() {
72                    self.lsp_sync_changed();
73                }
74                // Logical OSC52 consumption is common to both modes; the
75                // actual escape write stays the terminal adapter's
76                // live-only effect, recorded as a request — never an
77                // AppEvent, never a silent drop in replay.
78                if let Some(text) = self.osc52.take() {
79                    if self.tape.request("terminal.osc52", &text)? {
80                        self.terminal_output.push(text);
81                    }
82                }
83            }
84            Action::Frame { columns, rows } => {
85                if u32::from(columns) * u32::from(rows) > 1_000_000 {
86                    return Err(io::Error::other("invalid frame dimensions"));
87                }
88                if !self.should_quit && !self.docs.is_empty() {
89                    // Renders through the injected frame renderer: in
90                    // replay that draw consumes the recorded cell
91                    // observation. The binary installs the hook at its
92                    // composition roots (0046).
93                    let draw = self
94                        .frame_draw
95                        .ok_or_else(|| io::Error::other("frame draw hook not installed"))?;
96                    draw(self, columns, rows, false)?;
97                }
98            }
99            Action::Finish => {
100                self.finish_background_work();
101            }
102        }
103        self.tape.healthy()?;
104        if !frame && self.tape.observes() {
105            // Frames check the canonical cell grid instead; every other
106            // action checks the full logical observation.
107            self.tape.check(&self.observation())?;
108        }
109        Ok(())
110    }
111
112    /// The logical observation both modes must reproduce bit-for-bit.
113    /// Document content arrives as the pure `BufferSeed` (text, revision,
114    /// history, disk baseline) — diagnostic trace identities and other
115    /// process-local ephemera are deliberately absent.
116    fn observation(&self) -> serde_json::Value {
117        let documents: Vec<_> = self
118            .docs
119            .iter()
120            .map(
121                |(id, document)| serde_json::json!({"document": id, "buffer": document.buf.seed()}),
122            )
123            .collect();
124        serde_json::json!({
125            "documents": documents,
126            "panes": self.panes,
127            "mru": self.mru,
128            "active": self.active_pane,
129            "mode": self.mode.chip(),
130            "quit": self.should_quit,
131            "message": self.message,
132            "headless": crate::editor::state_json(self),
133            "hover": self.hover_card,
134            "hunks": self.hunks,
135            "staged_hunks": self.staged_hunks,
136            "picker_items": self.picker.as_ref().map(|glue| &glue.picker.items),
137        })
138    }
139}
140
141/// Replay a recorded forensic node stream end to end. The seed is taken
142/// first, the pure editor is reconstructed, and every recorded action is
143/// consumed through the shared body until the deliberate `End`; any
144/// divergence, gap or unexpected residue is an error, never a warning.
145/// The frame renderer replay checks recorded cells against; installed by
146/// the caller since cell-grid production lives outside the engine.
147pub fn replay(
148    nodes: Vec<strop_trace::replay::Node>,
149    frame_draw: crate::editor::FrameDraw,
150) -> io::Result<Editor> {
151    let tape = std::rc::Rc::new(strop_trace::replay::Tape::replay(nodes));
152    let seed: super::seed::Seed = tape.take_seed()?;
153    let mut editor = seed.into_editor(tape)?;
154    editor.frame_draw = Some(frame_draw);
155    while let Some(action) = editor.tape.next::<Action>()? {
156        editor.apply_recorded(action)?;
157    }
158    editor.tape.healthy()?;
159    Ok(editor)
160}