1use 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(Box<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(), |event| {
72 AppEvent::RemoteCompletion(Box::new(event))
73 });
74 }
75 if let Some(rx) = self.containers.take_rx() {
76 forward(rx, tx.clone(), AppEvent::Container);
77 }
78 if let Some(rx) = self.shell_rx.take() {
79 forward(rx, tx.clone(), AppEvent::Shell);
80 }
81 if let Some(rx) = self.git_rx.take() {
82 forward(rx, tx.clone(), AppEvent::Git);
83 }
84 if let Some(rx) = self.clip_rx.take() {
85 forward(rx, tx.clone(), AppEvent::Clipboard);
86 }
87 if let Some(rx) = self.preview_rx.take() {
88 forward(rx, tx.clone(), AppEvent::Preview);
89 }
90 if let Some(rx) = self.picker_ranking.rx.take() {
91 forward(rx, tx.clone(), AppEvent::PickerRanking);
92 }
93 if let Some(rx) = self.analysis.rx.take() {
94 forward(rx, tx.clone(), AppEvent::Analysis);
95 }
96 if let Some(rx) = self.resolution.rx.take() {
97 forward(rx, tx.clone(), AppEvent::Resolution);
98 }
99 self.connect_picker_stream(&tx);
100 for srv in &mut self.lsp_servers {
101 let rx = std::mem::replace(&mut srv.rx, std::sync::mpsc::channel().1);
102 forward(rx, tx.clone(), AppEvent::Lsp);
103 }
104 let rx = self.lsp_state.attach.take_rx();
105 forward(rx, tx.clone(), AppEvent::LspAttach);
106 self.app_tx = Some(tx);
107 }
108
109 pub fn handle_app_event(&mut self, ev: AppEvent) {
112 match ev {
113 AppEvent::Terminal(key) => self.feed(key),
114 AppEvent::Resize { .. } => {} AppEvent::Paste(text) => {
116 strop_trace::record_with(strop_trace::EventKind::Paste, || {
117 serde_json::json!({
118 "bytes":text.len(),"text":strop_trace::capture_content().then_some(text.as_str()),
119 })
120 });
121 if self.resolution.blocked() || !self.resolution.queue.is_empty() {
122 self.resolution
123 .queue
124 .push_back(super::resolution::DeferredInput::Paste(text));
125 return;
126 }
127 self.paste_bracketed(&text);
128 }
129 AppEvent::QuitIntent => {
130 strop_trace::record_with(
131 strop_trace::EventKind::Input,
132 || serde_json::json!({"action":"quit_intent","source":"external"}),
133 );
134 self.resolution.cancel();
135 self.resolution.queue.clear();
136 if self.ctrl_c_quit() {
137 self.should_quit = true;
138 }
139 }
140 AppEvent::Lsp(event) => self.handle_lsp_event(event),
141 AppEvent::LspAttach(record) => self.handle_lsp_attach(record),
142 AppEvent::Shell(r) => self.handle_shell_result(r),
143 AppEvent::Io(event) => self.handle_io(event),
144 AppEvent::RemoteCompletion(event) => self.handle_remote_completion(*event),
145 AppEvent::Container(event) => self.handle_container_event(event),
146 AppEvent::Git(job) => self.handle_git_job(job),
147 AppEvent::Picker(event) => self.handle_picker_event(event),
148 AppEvent::PickerRanking(event) => self.handle_picker_ranking(event),
149 AppEvent::Analysis(event) => self.handle_analysis(event),
150 AppEvent::Resolution(event) => self.handle_resolution(event),
151 AppEvent::ResumeInput => self.resume_resolution_input(),
152 AppEvent::Preview(result) => self.handle_preview(result),
153 AppEvent::Clipboard(content) => self.handle_clipboard(content),
154 }
155 }
156}
157
158impl Editor {
159 pub fn async_pending(&self) -> bool {
161 use strop_core::worker::Load;
162 self.io_pending()
163 || !self.shell_requests.is_empty()
164 || self.clip_paste_pending.is_some()
165 || self
166 .picker
167 .as_ref()
168 .is_some_and(|glue| glue.picker.streaming || glue.rank_pending.is_some())
169 || !self.picker_ranking.retiring.is_empty()
170 || self
171 .picker_source
172 .as_ref()
173 .is_some_and(strop_picker::SourceWorker::busy)
174 || self.analysis.pending()
175 || self.resolution.pending()
176 || self
177 .preview_loads
178 .values()
179 .any(|load| matches!(load, Load::Running(_)))
180 || matches!(self.git_discovery, Load::Running(_))
181 || matches!(self.hunk_load, Load::Running(_))
182 || !self.log_requests.is_empty()
183 || !self.dive_requests.is_empty()
184 || self.card_request.is_some()
185 || self.git_mutation.is_some()
186 || !self.git_mutations.is_empty()
187 || self
188 .blame_gutters
189 .values()
190 .any(|gutter| gutter.request.is_some())
191 || self.containers.pending.is_some()
192 || !self.lsp_state.attach.pending.is_empty()
193 || self.remote_completion.pending.is_some()
194 || (!self.finishing
195 && (self.lsp_state.hover.is_some()
196 || self.lsp_state.navigation.is_some()
197 || self.lsp_servers.iter().any(|server| !server.ready)))
198 }
199
200 pub(crate) fn finish_background_work(&mut self) {
203 self.finishing = true;
204 self.lsp_state.attach.enabled = false;
205 self.stop_remote_work();
206 self.close_picker();
207 if let Some(source) = self.picker_source.as_ref() {
208 source.close();
209 }
210 self.cancel_review_preparation();
211 self.analysis.stop();
212 self.resolution.stop();
213 self.git_mutations.clear();
214 self.request_session_save();
215 let cancel: Vec<_> = self
216 .worker_handles
217 .keys()
218 .copied()
219 .filter(|id| !self.io_write_pending(*id))
220 .collect();
221 for request in cancel {
222 if let Some(handle) = self.worker_handles.remove(&request) {
223 handle.cancel(strop_core::worker::CancelReason::Shutdown);
224 }
225 }
226 }
227
228 pub fn take_shutdown_error(&mut self) -> Option<String> {
230 match (
231 self.io.session_error.take(),
232 self.filesystem_shutdown_error(),
233 ) {
234 (Some(session), Some(filesystem)) => Some(format!("{session}\n{filesystem}")),
235 (Some(error), None) | (None, Some(error)) => Some(error),
236 (None, None) => None,
237 }
238 }
239}