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::Remote(event) => {
61            use crate::editor::remote::RemoteEvent;
62            match event {
63                RemoteEvent::Tick(ticket) => {
64                    json!({"service":"remote","result":"follow_tick","ticket":ticket})
65                }
66                RemoteEvent::Timer(value) => completion("remote", "follow_clock", value),
67                RemoteEvent::Read(value) => completion("remote", "follow_read", value),
68                RemoteEvent::Control(value) => completion("remote", "control", value),
69                RemoteEvent::Filter(value) => completion("remote", "directory_filter", value),
70                RemoteEvent::Choices(value) => completion("remote", "destinations", value),
71                RemoteEvent::Write(value) => {
72                    let mut record = completion("remote", "write", value);
73                    if let strop_core::worker::Outcome::Success(
74                        crate::editor::remote::save::RemoteWriteResult::Refused(error),
75                    ) = &value.outcome
76                    {
77                        record["outcome"] = outcome(&strop_core::worker::Outcome::<()>::failed(
78                            strop_core::worker::FailureKind::Io,
79                            error.to_string(),
80                        ));
81                    }
82                    record
83                }
84                RemoteEvent::DestinationWritten(value) => {
85                    completion("remote", "destination_write", value)
86                }
87            }
88        }
89        IoEvent::Session {
90            request,
91            outcome: value,
92        } => json!({"service":"io","result":"session","request":request,"outcome":outcome(value)}),
93    });
94}
95
96pub fn shell(result: &ShellResult) {
97    record_with(EventKind::JobFinished, || {
98        let key = match &result.ticket.key {
99            crate::editor::ShellKey::Display {
100                origin, revision, ..
101            } => json!({
102                "kind":"display","document":super::snapshot::document_id(*origin),
103                "revision":revision,
104            }),
105            crate::editor::ShellKey::Pipe {
106                document,
107                revision,
108                start,
109                end,
110            } => json!({
111                "kind":"pipe","document":super::snapshot::document_id(*document),
112                "revision":revision,"start_byte":start,"end_byte":end,
113            }),
114        };
115        let outcome = match &result.outcome {
116            strop_core::worker::Outcome::Success(output) => json!({
117                "stdout_bytes":output.stdout.len(),"stderr_bytes":output.stderr.len(),
118            }),
119            strop_core::worker::Outcome::Failed { failure, .. } => {
120                json!({"error": failure.message})
121            }
122            _ => json!({"cancelled": true}),
123        };
124        json!({"service":"shell","request":result.ticket.request.get(),
125            "key":key,"outcome":outcome})
126    });
127}
128
129pub fn clipboard(result: &ClipboardResult) {
130    record_with(EventKind::JobFinished, || {
131        let outcome = match &result.outcome {
132            strop_core::worker::Outcome::Success(text) => json!({"bytes": text.len()}),
133            strop_core::worker::Outcome::Failed { failure, .. } => {
134                json!({"error": failure.message})
135            }
136            _ => json!({"cancelled": true}),
137        };
138        json!({"service":"clipboard","request":result.ticket.request.get(),
139            "document":super::snapshot::document_id(result.ticket.key.document),
140            "outcome":outcome})
141    });
142}
143
144pub fn lsp(event: &strop_lsp::LspEvent) {
145    use strop_lsp::LspEvent;
146    record_with(EventKind::JobFinished, || {
147        if strop_trace::capture_content() {
148            return json!({"service":"lsp", "event":event});
149        }
150        match event {
151            LspEvent::Ready { server, name } => {
152                json!({"service":"lsp","result":"ready","server":server,"name":name})
153            }
154            LspEvent::Failed { server, name, hint } => {
155                // The hint carries the executable and the fix (0033 §3)
156                json!({"service":"lsp","result":"failed","server":server,"name":name,"hint":hint})
157            }
158            LspEvent::ServerMessage { server, name, text } => {
159                json!({"service":"lsp","result":"message","server":server,"name":name,"bytes":text.len()})
160            }
161            LspEvent::Diagnostics { context, diags, .. } => json!({
162                "service":"lsp","result":"diagnostics","context":context,"count":diags.len(),
163            }),
164            LspEvent::Edits { context, edits } => json!({
165                "service":"lsp","result":"edits","context":context,"count":edits.len(),
166            }),
167            LspEvent::WorkspaceEdits { context, edits } => json!({
168                "service":"lsp","result":"workspace_edits","context":context,"targets":edits.len(),
169            }),
170            LspEvent::Symbols { context, symbols } => json!({
171                "service":"lsp","result":"symbols","context":context,"count":symbols.len(),
172            }),
173            LspEvent::ActionList { context, actions } => json!({
174                "service":"lsp","result":"code_actions","context":context,"count":actions.len(),
175            }),
176            LspEvent::HoverText { context, text } => json!({
177                "service":"lsp","result":"hover","context":context,"bytes":text.len(),
178            }),
179            LspEvent::Note { context, text } => json!({
180                "service":"lsp","result":"note","context":context,"bytes":text.len(),
181            }),
182            LspEvent::GotoLocation { context, .. } => json!({
183                "service":"lsp","result":"goto","context":context,
184            }),
185            LspEvent::Locations {
186                context,
187                kind,
188                items,
189            } => json!({
190                "service":"lsp","result":kind.label(),"context":context,"count":items.len(),
191            }),
192        }
193    });
194}
195
196pub fn picker(event: &crate::editor::picker::PickerEvent) {
197    use strop_picker::PickerMsg;
198    record_with(EventKind::JobFinished, || {
199        let outcome = match &event.msg {
200            PickerMsg::Items(items) => json!({"result":"items","count":items.len()}),
201            PickerMsg::Warning(warning) => json!({"result":"warning","bytes":warning.len()}),
202            PickerMsg::QueryError(diagnostic) => {
203                json!({"result":"query-error","range":diagnostic.range})
204            }
205            PickerMsg::Finished(outcome) => match outcome {
206                strop_core::worker::Outcome::Success(()) => json!({"result":"finished"}),
207                strop_core::worker::Outcome::Failed { failure, .. } => {
208                    json!({"result":"error","error":failure.message})
209                }
210                _ => json!({"result":"cancelled"}),
211            },
212        };
213        json!({"service":"picker","request":event.ticket.request.get(),
214            "outcome":outcome})
215    });
216}