Skip to main content

strop_engine/editor/trace/
services.rs

1//! Shared service-handler producers: TUI deliveries and headless drains both
2//! reach these handlers. Never record raw clipboard or shell-output payloads.
3//!
4//! `NativePath` is the tape's path-carrying argument wrapper: native paths
5//! serialize through strop-core's versioned encoding so replay never depends
6//! on lossy UTF-8 conversion.
7use crate::editor::git_memory::GitJob;
8use crate::editor::{ClipboardResult, ShellResult};
9use serde_json::json;
10use strop_trace::{record_with, EventKind};
11
12/// A path crossing the tape boundary. Reusable where a tuple carries a
13/// path; structs with path fields use `#[serde(with=...)]` directly.
14#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15pub struct NativePath(#[serde(with = "strop_core::path_serde")] pub std::path::PathBuf);
16
17pub fn rejected(service: &'static str, reason: &str) {
18    record_with(
19        EventKind::JobRejected,
20        || json!({"service":service,"reason":reason}),
21    );
22}
23fn outcome<T>(value: &strop_core::worker::Outcome<T>) -> serde_json::Value {
24    use strop_core::worker::Outcome;
25    match value {
26        Outcome::Success(_) => json!({"kind":"success"}),
27        Outcome::Failed { failure, partial } => {
28            json!({"kind":"failed","failure":failure,"partial":partial.is_some()})
29        }
30        Outcome::Cancelled(reason) => json!({"kind":"cancelled","reason":reason}),
31    }
32}
33
34fn completion<K: serde::Serialize, T>(
35    service: &str,
36    result: &str,
37    value: &strop_core::worker::Completion<K, T>,
38) -> serde_json::Value {
39    json!({"service":service,"result":result,"ticket":value.ticket,"outcome":outcome(&value.outcome)})
40}
41
42pub fn git(job: &GitJob) {
43    record_with(EventKind::JobFinished, || match job {
44        GitJob::Context(value) => completion("git", "context", value),
45        GitJob::Hunks(value) => completion("git", "hunks", value),
46        GitJob::Mutation(value) => completion("git", "mutation", value),
47        GitJob::Log(value) => completion("git", "log", value),
48        GitJob::Gutter(value) => completion("git", "gutter", value),
49        GitJob::Card(value) => completion("git", "blame_card", value),
50        GitJob::Dive(value) => completion("git", "dive", value),
51    });
52}
53
54pub fn io(event: &crate::editor::io::IoEvent) {
55    use crate::editor::io::IoEvent;
56    record_with(EventKind::JobFinished, || match event {
57        IoEvent::Open(value) => completion("io", "open", value),
58        IoEvent::Save(value) => completion("io", "save", value),
59        IoEvent::Native(value) => completion("io", "native", value),
60        IoEvent::Review(value) => completion("search", "review", value),
61        IoEvent::DirectoryFilter(value) => completion("directory", "filter", value),
62        IoEvent::Filesystem(event) => {
63            use crate::editor::filesystem::FsEvent;
64            match event.as_ref() {
65                FsEvent::Prepared(value) => completion("filesystem", "prepare", value),
66                FsEvent::Applied(value) => completion("filesystem", "apply", value),
67                FsEvent::Verified(value) => completion("filesystem", "verify", value),
68            }
69        }
70        IoEvent::Remote(event) => {
71            use crate::editor::remote::RemoteEvent;
72            match event {
73                RemoteEvent::Tick(ticket) => {
74                    json!({"service":"remote","result":"follow_tick","ticket":ticket})
75                }
76                RemoteEvent::Timer(value) => completion("remote", "follow_clock", value),
77                RemoteEvent::Read(value) => completion("remote", "follow_read", value),
78                RemoteEvent::Control(value) => completion("remote", "control", value),
79                RemoteEvent::Choices(value) => completion("remote", "destinations", value),
80                RemoteEvent::Write(value) => {
81                    let mut record = completion("remote", "write", value);
82                    if let strop_core::worker::Outcome::Success(
83                        crate::editor::remote::save::RemoteWriteResult::Refused(error),
84                    ) = &value.outcome
85                    {
86                        record["outcome"] = outcome(&strop_core::worker::Outcome::<()>::failed(
87                            strop_core::worker::FailureKind::Io,
88                            error.to_string(),
89                        ));
90                    }
91                    record
92                }
93                RemoteEvent::DestinationWritten(value) => {
94                    completion("remote", "destination_write", value)
95                }
96            }
97        }
98        IoEvent::Session {
99            request,
100            outcome: value,
101        } => json!({"service":"io","result":"session","request":request,"outcome":outcome(value)}),
102    });
103}
104
105pub fn shell(result: &ShellResult) {
106    record_with(EventKind::JobFinished, || {
107        let key = match &result.ticket.key {
108            crate::editor::ShellKey::Display {
109                origin, revision, ..
110            } => json!({
111                "kind":"display","document":super::snapshot::document_id(*origin),
112                "revision":revision,
113            }),
114            crate::editor::ShellKey::Pipe {
115                document,
116                revision,
117                start,
118                end,
119            } => json!({
120                "kind":"pipe","document":super::snapshot::document_id(*document),
121                "revision":revision,"start_byte":start,"end_byte":end,
122            }),
123        };
124        let outcome = match &result.outcome {
125            strop_core::worker::Outcome::Success(output) => json!({
126                "stdout_bytes":output.stdout.len(),"stderr_bytes":output.stderr.len(),
127            }),
128            strop_core::worker::Outcome::Failed { failure, .. } => {
129                json!({"error": failure.message})
130            }
131            _ => json!({"cancelled": true}),
132        };
133        json!({"service":"shell","request":result.ticket.request.get(),
134            "key":key,"outcome":outcome})
135    });
136}
137
138pub fn clipboard(result: &ClipboardResult) {
139    record_with(EventKind::JobFinished, || {
140        let outcome = match &result.outcome {
141            strop_core::worker::Outcome::Success(text) => json!({"bytes": text.len()}),
142            strop_core::worker::Outcome::Failed { failure, .. } => {
143                json!({"error": failure.message})
144            }
145            _ => json!({"cancelled": true}),
146        };
147        json!({"service":"clipboard","request":result.ticket.request.get(),
148            "document":super::snapshot::document_id(result.ticket.key.document),
149            "outcome":outcome})
150    });
151}
152
153pub fn lsp(event: &strop_lsp::LspEvent) {
154    use strop_lsp::LspEvent;
155    record_with(EventKind::JobFinished, || {
156        if strop_trace::capture_content() {
157            return json!({"service":"lsp", "event":event});
158        }
159        match event {
160            LspEvent::Ready { server, name } => {
161                json!({"service":"lsp","result":"ready","server":server,"name":name})
162            }
163            LspEvent::Failed { server, name, hint } => {
164                // The hint carries the executable and the fix (0033 §3)
165                json!({"service":"lsp","result":"failed","server":server,"name":name,"hint":hint})
166            }
167            LspEvent::ServerMessage { server, name, text } => {
168                json!({"service":"lsp","result":"message","server":server,"name":name,"bytes":text.len()})
169            }
170            LspEvent::Diagnostics { context, diags, .. } => json!({
171                "service":"lsp","result":"diagnostics","context":context,"count":diags.len(),
172            }),
173            LspEvent::Edits { context, edits } => json!({
174                "service":"lsp","result":"edits","context":context,"count":edits.len(),
175            }),
176            LspEvent::WorkspaceEdits { context, edits } => json!({
177                "service":"lsp","result":"workspace_edits","context":context,"targets":edits.len(),
178            }),
179            LspEvent::Symbols { context, symbols } => json!({
180                "service":"lsp","result":"symbols","context":context,"count":symbols.len(),
181            }),
182            LspEvent::ActionList { context, actions } => json!({
183                "service":"lsp","result":"code_actions","context":context,"count":actions.len(),
184            }),
185            LspEvent::HoverText { context, text } => json!({
186                "service":"lsp","result":"hover","context":context,"bytes":text.len(),
187            }),
188            LspEvent::Note { context, text } => json!({
189                "service":"lsp","result":"note","context":context,"bytes":text.len(),
190            }),
191            LspEvent::GotoLocation { context, .. } => json!({
192                "service":"lsp","result":"goto","context":context,
193            }),
194            LspEvent::Locations {
195                context,
196                kind,
197                items,
198            } => json!({
199                "service":"lsp","result":kind.label(),"context":context,"count":items.len(),
200            }),
201        }
202    });
203}
204
205pub fn picker(event: &crate::editor::picker::PickerEvent) {
206    use strop_picker::PickerMsg;
207    record_with(EventKind::JobFinished, || {
208        let outcome = match &event.msg {
209            PickerMsg::Items(items) => json!({"result":"items","count":items.len()}),
210            PickerMsg::Warning(warning) => json!({"result":"warning","bytes":warning.len()}),
211            PickerMsg::QueryError(diagnostic) => {
212                json!({"result":"query-error","range":diagnostic.range})
213            }
214            PickerMsg::Finished(outcome) => match outcome {
215                strop_core::worker::Outcome::Success(()) => json!({"result":"finished"}),
216                strop_core::worker::Outcome::Failed { failure, .. } => {
217                    json!({"result":"error","error":failure.message})
218                }
219                _ => json!({"result":"cancelled"}),
220            },
221        };
222        json!({"service":"picker","request":event.ticket.request.get(),
223            "outcome":outcome})
224    });
225}