strop_engine/editor/
events.rs1use std::sync::mpsc::Receiver;
12mod channel;
13pub use channel::{channel, EventSender, EVENTS_PER_TURN, TURN_BUDGET};
14
15use super::{Editor, Key, ShellResult};
16
17#[derive(serde::Serialize, serde::Deserialize)]
20pub enum AppEvent {
21 Terminal(Key),
22 Resize {
24 columns: u16,
25 rows: u16,
26 },
27 Paste(String),
29 QuitIntent,
31 Lsp(strop_lsp::LspEvent),
32 LspAttach(super::lsp::attach::AttachRecord),
33 Shell(ShellResult),
34 Io(super::io::IoEvent),
35 RemoteCompletion(super::remote_completion::RemoteCompletionEvent),
36 Container(super::containers::ContainerEvent),
37 Git(super::GitJob),
38 Picker(super::picker::PickerEvent),
39 PickerRanking(super::picker::ranking::Event),
40 Analysis(super::analysis::AnalysisEvent),
41 Resolution(super::resolution::ResolutionEvent),
42 ResumeInput,
43 Preview(super::picker::PreviewResult),
44 Clipboard(super::ClipboardResult),
45}
46
47fn forward<T: Send + 'static>(
49 rx: Receiver<T>,
50 tx: EventSender,
51 wrap: impl Fn(T) -> AppEvent + Send + 'static,
52) {
53 std::thread::spawn(move || {
54 while let Ok(item) = rx.recv() {
55 if tx.send(wrap(item)).is_err() {
56 break;
57 }
58 }
59 });
60}
61
62impl Editor {
63 pub fn connect_events(&mut self, tx: EventSender) {
67 if let Some(rx) = self.io.rx.take() {
68 forward(rx, tx.clone(), AppEvent::Io);
69 }
70 if let Some(rx) = self.remote_completion.rx.take() {
71 forward(rx, tx.clone(), AppEvent::RemoteCompletion);
72 }
73 if let Some(rx) = self.containers.take_rx() {
74 forward(rx, tx.clone(), AppEvent::Container);
75 }
76 if let Some(rx) = self.shell_rx.take() {
77 forward(rx, tx.clone(), AppEvent::Shell);
78 }
79 if let Some(rx) = self.git_rx.take() {
80 forward(rx, tx.clone(), AppEvent::Git);
81 }
82 if let Some(rx) = self.clip_rx.take() {
83 forward(rx, tx.clone(), AppEvent::Clipboard);
84 }
85 if let Some(rx) = self.preview_rx.take() {
86 forward(rx, tx.clone(), AppEvent::Preview);
87 }
88 if let Some(rx) = self.picker_ranking.rx.take() {
89 forward(rx, tx.clone(), AppEvent::PickerRanking);
90 }
91 if let Some(rx) = self.analysis.rx.take() {
92 forward(rx, tx.clone(), AppEvent::Analysis);
93 }
94 if let Some(rx) = self.resolution.rx.take() {
95 forward(rx, tx.clone(), AppEvent::Resolution);
96 }
97 self.connect_picker_stream(&tx);
98 for srv in &mut self.lsp_servers {
99 let rx = std::mem::replace(&mut srv.rx, std::sync::mpsc::channel().1);
100 forward(rx, tx.clone(), AppEvent::Lsp);
101 }
102 let rx = self.lsp_state.attach.take_rx();
103 forward(rx, tx.clone(), AppEvent::LspAttach);
104 self.app_tx = Some(tx);
105 }
106
107 pub fn handle_app_event(&mut self, ev: AppEvent) {
110 match ev {
111 AppEvent::Terminal(key) => self.feed(key),
112 AppEvent::Resize { .. } => {} AppEvent::Paste(text) => {
114 strop_trace::record_with(strop_trace::EventKind::Paste, || {
115 serde_json::json!({
116 "bytes":text.len(),"text":strop_trace::capture_content().then_some(text.as_str()),
117 })
118 });
119 if self.resolution.blocked() || !self.resolution.queue.is_empty() {
120 self.resolution
121 .queue
122 .push_back(super::resolution::DeferredInput::Paste(text));
123 return;
124 }
125 self.paste_bracketed(&text);
126 }
127 AppEvent::QuitIntent => {
128 strop_trace::record_with(
129 strop_trace::EventKind::Input,
130 || serde_json::json!({"action":"quit_intent","source":"external"}),
131 );
132 self.resolution.cancel();
133 self.resolution.queue.clear();
134 if self.ctrl_c_quit() {
135 self.should_quit = true;
136 }
137 }
138 AppEvent::Lsp(event) => self.handle_lsp_event(event),
139 AppEvent::LspAttach(record) => self.handle_lsp_attach(record),
140 AppEvent::Shell(r) => self.handle_shell_result(r),
141 AppEvent::Io(event) => self.handle_io(event),
142 AppEvent::RemoteCompletion(event) => self.handle_remote_completion(event),
143 AppEvent::Container(event) => self.handle_container_event(event),
144 AppEvent::Git(job) => self.handle_git_job(job),
145 AppEvent::Picker(event) => self.handle_picker_event(event),
146 AppEvent::PickerRanking(event) => self.handle_picker_ranking(event),
147 AppEvent::Analysis(event) => self.handle_analysis(event),
148 AppEvent::Resolution(event) => self.handle_resolution(event),
149 AppEvent::ResumeInput => self.resume_resolution_input(),
150 AppEvent::Preview(result) => self.handle_preview(result),
151 AppEvent::Clipboard(content) => self.handle_clipboard(content),
152 }
153 }
154}
155
156impl Editor {
157 pub fn async_pending(&self) -> bool {
159 use strop_core::worker::Load;
160 self.io_pending()
161 || !self.shell_requests.is_empty()
162 || self.clip_paste_pending.is_some()
163 || self
164 .picker
165 .as_ref()
166 .is_some_and(|glue| glue.picker.streaming || glue.rank_pending.is_some())
167 || !self.picker_ranking.retiring.is_empty()
168 || self.analysis.pending()
169 || self.resolution.pending()
170 || self
171 .preview_loads
172 .values()
173 .any(|load| matches!(load, Load::Running(_)))
174 || matches!(self.git_discovery, Load::Running(_))
175 || matches!(self.hunk_load, Load::Running(_))
176 || !self.log_requests.is_empty()
177 || !self.dive_requests.is_empty()
178 || self.card_request.is_some()
179 || self.git_mutation.is_some()
180 || !self.git_mutations.is_empty()
181 || self
182 .blame_gutters
183 .values()
184 .any(|gutter| gutter.request.is_some())
185 || self.containers.pending.is_some()
186 || !self.lsp_state.attach.pending.is_empty()
187 || self.remote_completion.pending.is_some()
188 || (!self.finishing
189 && (self.lsp_state.hover.is_some()
190 || self.lsp_state.navigation.is_some()
191 || self.lsp_servers.iter().any(|server| !server.ready)))
192 }
193
194 pub(crate) fn finish_background_work(&mut self) {
197 self.finishing = true;
198 self.lsp_state.attach.enabled = false;
199 self.stop_remote_work();
200 self.close_picker();
201 self.cancel_review_preparation();
202 self.analysis.stop();
203 self.resolution.stop();
204 self.git_mutations.clear();
205 self.request_session_save();
206 let cancel: Vec<_> = self
207 .worker_handles
208 .keys()
209 .copied()
210 .filter(|id| !self.io_write_pending(*id))
211 .collect();
212 for request in cancel {
213 if let Some(handle) = self.worker_handles.remove(&request) {
214 handle.cancel(strop_core::worker::CancelReason::Shutdown);
215 }
216 }
217 }
218}