running.rs

  1mod console;
  2mod loaded_source_list;
  3mod module_list;
  4pub mod stack_frame_list;
  5pub mod variable_list;
  6
  7use super::{DebugPanelItemEvent, ThreadItem};
  8use console::Console;
  9use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
 10use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
 11use loaded_source_list::LoadedSourceList;
 12use module_list::ModuleList;
 13use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
 14use rpc::proto::ViewId;
 15use settings::Settings;
 16use stack_frame_list::StackFrameList;
 17use ui::{
 18    ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder,
 19    Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
 20    StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex,
 21};
 22use util::ResultExt;
 23use variable_list::VariableList;
 24use workspace::Workspace;
 25
 26pub struct RunningState {
 27    session: Entity<Session>,
 28    thread_id: Option<ThreadId>,
 29    console: Entity<console::Console>,
 30    focus_handle: FocusHandle,
 31    _remote_id: Option<ViewId>,
 32    show_console_indicator: bool,
 33    module_list: Entity<module_list::ModuleList>,
 34    active_thread_item: ThreadItem,
 35    workspace: WeakEntity<Workspace>,
 36    session_id: SessionId,
 37    variable_list: Entity<variable_list::VariableList>,
 38    _subscriptions: Vec<Subscription>,
 39    stack_frame_list: Entity<stack_frame_list::StackFrameList>,
 40    loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
 41}
 42
 43impl Render for RunningState {
 44    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 45        let threads = self.session.update(cx, |this, cx| this.threads(cx));
 46        self.select_current_thread(&threads, cx);
 47
 48        let thread_status = self
 49            .thread_id
 50            .map(|thread_id| self.session.read(cx).thread_status(thread_id))
 51            .unwrap_or(ThreadStatus::Exited);
 52
 53        self.variable_list.update(cx, |this, cx| {
 54            this.disabled(thread_status != ThreadStatus::Stopped, cx);
 55        });
 56
 57        let active_thread_item = &self.active_thread_item;
 58
 59        let capabilities = self.capabilities(cx);
 60        h_flex()
 61            .key_context("DebugPanelItem")
 62            .track_focus(&self.focus_handle(cx))
 63            .size_full()
 64            .items_start()
 65            .child(
 66                v_flex().size_full().items_start().child(
 67                    h_flex()
 68                        .size_full()
 69                        .items_start()
 70                        .p_1()
 71                        .gap_4()
 72                        .child(self.stack_frame_list.clone()),
 73                ),
 74            )
 75            .child(
 76                v_flex()
 77                    .border_l_1()
 78                    .border_color(cx.theme().colors().border_variant)
 79                    .size_full()
 80                    .items_start()
 81                    .child(
 82                        h_flex()
 83                            .border_b_1()
 84                            .w_full()
 85                            .border_color(cx.theme().colors().border_variant)
 86                            .child(self.render_entry_button(
 87                                &SharedString::from("Variables"),
 88                                ThreadItem::Variables,
 89                                cx,
 90                            ))
 91                            .when(
 92                                capabilities.supports_modules_request.unwrap_or_default(),
 93                                |this| {
 94                                    this.child(self.render_entry_button(
 95                                        &SharedString::from("Modules"),
 96                                        ThreadItem::Modules,
 97                                        cx,
 98                                    ))
 99                                },
100                            )
101                            .when(
102                                capabilities
103                                    .supports_loaded_sources_request
104                                    .unwrap_or_default(),
105                                |this| {
106                                    this.child(self.render_entry_button(
107                                        &SharedString::from("Loaded Sources"),
108                                        ThreadItem::LoadedSource,
109                                        cx,
110                                    ))
111                                },
112                            )
113                            .child(self.render_entry_button(
114                                &SharedString::from("Console"),
115                                ThreadItem::Console,
116                                cx,
117                            )),
118                    )
119                    .when(*active_thread_item == ThreadItem::Variables, |this| {
120                        this.child(self.variable_list.clone())
121                    })
122                    .when(*active_thread_item == ThreadItem::Modules, |this| {
123                        this.size_full().child(self.module_list.clone())
124                    })
125                    .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
126                        this.size_full().child(self.loaded_source_list.clone())
127                    })
128                    .when(*active_thread_item == ThreadItem::Console, |this| {
129                        this.child(self.console.clone())
130                    }),
131            )
132    }
133}
134
135impl RunningState {
136    pub fn new(
137        session: Entity<Session>,
138        workspace: WeakEntity<Workspace>,
139        window: &mut Window,
140        cx: &mut Context<Self>,
141    ) -> Self {
142        let focus_handle = cx.focus_handle();
143        let session_id = session.read(cx).session_id();
144        let weak_state = cx.weak_entity();
145        let stack_frame_list = cx.new(|cx| {
146            StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
147        });
148
149        let variable_list =
150            cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
151
152        let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
153
154        let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
155
156        let console = cx.new(|cx| {
157            Console::new(
158                session.clone(),
159                stack_frame_list.clone(),
160                variable_list.clone(),
161                window,
162                cx,
163            )
164        });
165
166        let _subscriptions = vec![
167            cx.observe(&module_list, |_, _, cx| cx.notify()),
168            cx.subscribe_in(&session, window, |this, _, event, window, cx| {
169                match event {
170                    SessionEvent::Stopped(thread_id) => {
171                        this.workspace
172                            .update(cx, |workspace, cx| {
173                                workspace.open_panel::<crate::DebugPanel>(window, cx);
174                            })
175                            .log_err();
176
177                        if let Some(thread_id) = thread_id {
178                            this.select_thread(*thread_id, cx);
179                        }
180                    }
181                    SessionEvent::Threads => {
182                        let threads = this.session.update(cx, |this, cx| this.threads(cx));
183                        this.select_current_thread(&threads, cx);
184                    }
185                    _ => {}
186                }
187                cx.notify()
188            }),
189        ];
190
191        Self {
192            session,
193            console,
194            workspace,
195            module_list,
196            focus_handle,
197            variable_list,
198            _subscriptions,
199            thread_id: None,
200            _remote_id: None,
201            stack_frame_list,
202            loaded_source_list,
203            session_id,
204            show_console_indicator: false,
205            active_thread_item: ThreadItem::Variables,
206        }
207    }
208
209    pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
210        if self.thread_id.is_some() {
211            self.stack_frame_list
212                .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
213        }
214    }
215
216    pub fn session(&self) -> &Entity<Session> {
217        &self.session
218    }
219
220    pub fn session_id(&self) -> SessionId {
221        self.session_id
222    }
223
224    #[cfg(test)]
225    pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
226        self.active_thread_item = thread_item;
227        cx.notify()
228    }
229
230    #[cfg(test)]
231    pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
232        &self.stack_frame_list
233    }
234
235    #[cfg(test)]
236    pub fn console(&self) -> &Entity<Console> {
237        &self.console
238    }
239
240    #[cfg(test)]
241    pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
242        &self.module_list
243    }
244
245    #[cfg(test)]
246    pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
247        &self.variable_list
248    }
249
250    pub fn capabilities(&self, cx: &App) -> Capabilities {
251        self.session().read(cx).capabilities().clone()
252    }
253
254    pub fn select_current_thread(
255        &mut self,
256        threads: &Vec<(Thread, ThreadStatus)>,
257        cx: &mut Context<Self>,
258    ) {
259        let selected_thread = self
260            .thread_id
261            .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
262            .or_else(|| threads.first());
263
264        let Some((selected_thread, _)) = selected_thread else {
265            return;
266        };
267
268        if Some(ThreadId(selected_thread.id)) != self.thread_id {
269            self.select_thread(ThreadId(selected_thread.id), cx);
270        }
271    }
272
273    #[cfg(test)]
274    pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
275        self.thread_id
276    }
277
278    pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
279        self.thread_id
280            .map(|id| self.session().read(cx).thread_status(id))
281    }
282
283    fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
284        if self.thread_id.is_some_and(|id| id == thread_id) {
285            return;
286        }
287
288        self.thread_id = Some(thread_id);
289
290        self.stack_frame_list
291            .update(cx, |list, cx| list.refresh(cx));
292        cx.notify();
293    }
294
295    fn render_entry_button(
296        &self,
297        label: &SharedString,
298        thread_item: ThreadItem,
299        cx: &mut Context<Self>,
300    ) -> AnyElement {
301        let has_indicator =
302            matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
303
304        div()
305            .id(label.clone())
306            .px_2()
307            .py_1()
308            .cursor_pointer()
309            .border_b_2()
310            .when(self.active_thread_item == thread_item, |this| {
311                this.border_color(cx.theme().colors().border)
312            })
313            .child(
314                h_flex()
315                    .child(Button::new(label.clone(), label.clone()))
316                    .when(has_indicator, |this| this.child(Indicator::dot())),
317            )
318            .on_click(cx.listener(move |this, _, _window, cx| {
319                this.active_thread_item = thread_item;
320
321                if matches!(this.active_thread_item, ThreadItem::Console) {
322                    this.show_console_indicator = false;
323                }
324
325                cx.notify();
326            }))
327            .into_any_element()
328    }
329
330    pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
331        let Some(thread_id) = self.thread_id else {
332            return;
333        };
334
335        self.session().update(cx, |state, cx| {
336            state.continue_thread(thread_id, cx);
337        });
338    }
339
340    pub fn step_over(&mut self, cx: &mut Context<Self>) {
341        let Some(thread_id) = self.thread_id else {
342            return;
343        };
344
345        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
346
347        self.session().update(cx, |state, cx| {
348            state.step_over(thread_id, granularity, cx);
349        });
350    }
351
352    pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
353        let Some(thread_id) = self.thread_id else {
354            return;
355        };
356
357        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
358
359        self.session().update(cx, |state, cx| {
360            state.step_in(thread_id, granularity, cx);
361        });
362    }
363
364    pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
365        let Some(thread_id) = self.thread_id else {
366            return;
367        };
368
369        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
370
371        self.session().update(cx, |state, cx| {
372            state.step_out(thread_id, granularity, cx);
373        });
374    }
375
376    pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
377        let Some(thread_id) = self.thread_id else {
378            return;
379        };
380
381        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
382
383        self.session().update(cx, |state, cx| {
384            state.step_back(thread_id, granularity, cx);
385        });
386    }
387
388    pub fn restart_session(&self, cx: &mut Context<Self>) {
389        self.session().update(cx, |state, cx| {
390            state.restart(None, cx);
391        });
392    }
393
394    pub fn pause_thread(&self, cx: &mut Context<Self>) {
395        let Some(thread_id) = self.thread_id else {
396            return;
397        };
398
399        self.session().update(cx, |state, cx| {
400            state.pause_thread(thread_id, cx);
401        });
402    }
403
404    pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
405        self.workspace
406            .update(cx, |workspace, cx| {
407                workspace
408                    .project()
409                    .read(cx)
410                    .breakpoint_store()
411                    .update(cx, |store, cx| {
412                        store.remove_active_position(Some(self.session_id), cx)
413                    })
414            })
415            .log_err();
416
417        self.session.update(cx, |session, cx| {
418            session.shutdown(cx).detach();
419        })
420    }
421
422    pub fn stop_thread(&self, cx: &mut Context<Self>) {
423        let Some(thread_id) = self.thread_id else {
424            return;
425        };
426
427        self.workspace
428            .update(cx, |workspace, cx| {
429                workspace
430                    .project()
431                    .read(cx)
432                    .breakpoint_store()
433                    .update(cx, |store, cx| {
434                        store.remove_active_position(Some(self.session_id), cx)
435                    })
436            })
437            .log_err();
438
439        self.session().update(cx, |state, cx| {
440            state.terminate_threads(Some(vec![thread_id; 1]), cx);
441        });
442    }
443
444    #[expect(
445        unused,
446        reason = "Support for disconnecting a client is not wired through yet"
447    )]
448    pub fn disconnect_client(&self, cx: &mut Context<Self>) {
449        self.session().update(cx, |state, cx| {
450            state.disconnect_client(cx);
451        });
452    }
453
454    pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
455        self.session.update(cx, |session, cx| {
456            session.toggle_ignore_breakpoints(cx).detach();
457        });
458    }
459
460    pub(crate) fn thread_dropdown(
461        &self,
462        window: &mut Window,
463        cx: &mut Context<'_, RunningState>,
464    ) -> DropdownMenu {
465        let state = cx.entity();
466        let threads = self.session.update(cx, |this, cx| this.threads(cx));
467        let selected_thread_name = threads
468            .iter()
469            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
470            .map(|(thread, _)| thread.name.clone())
471            .unwrap_or("Threads".to_owned());
472        DropdownMenu::new(
473            ("thread-list", self.session_id.0),
474            selected_thread_name,
475            ContextMenu::build(window, cx, move |mut this, _, _| {
476                for (thread, _) in threads {
477                    let state = state.clone();
478                    let thread_id = thread.id;
479                    this = this.entry(thread.name, None, move |_, cx| {
480                        state.update(cx, |state, cx| {
481                            state.select_thread(ThreadId(thread_id), cx);
482                        });
483                    });
484                }
485                this
486            }),
487        )
488    }
489}
490
491impl EventEmitter<DebugPanelItemEvent> for RunningState {}
492
493impl Focusable for RunningState {
494    fn focus_handle(&self, _: &App) -> FocusHandle {
495        self.focus_handle.clone()
496    }
497}