running.rs

  1mod console;
  2mod loaded_source_list;
  3mod module_list;
  4pub mod stack_frame_list;
  5pub mod variable_list;
  6
  7use std::{any::Any, ops::ControlFlow, sync::Arc};
  8
  9use super::DebugPanelItemEvent;
 10use collections::HashMap;
 11use console::Console;
 12use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
 13use gpui::{
 14    Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
 15    NoAction, Subscription, WeakEntity,
 16};
 17use loaded_source_list::LoadedSourceList;
 18use module_list::ModuleList;
 19use project::{
 20    Project,
 21    debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
 22};
 23use rpc::proto::ViewId;
 24use settings::Settings;
 25use stack_frame_list::StackFrameList;
 26use ui::{
 27    App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, ParentElement,
 28    Render, SharedString, Styled, Window, div, h_flex, v_flex,
 29};
 30use util::ResultExt;
 31use variable_list::VariableList;
 32use workspace::{
 33    ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, move_item, pane::Event,
 34};
 35
 36pub struct RunningState {
 37    session: Entity<Session>,
 38    thread_id: Option<ThreadId>,
 39    focus_handle: FocusHandle,
 40    _remote_id: Option<ViewId>,
 41    workspace: WeakEntity<Workspace>,
 42    session_id: SessionId,
 43    variable_list: Entity<variable_list::VariableList>,
 44    _subscriptions: Vec<Subscription>,
 45    stack_frame_list: Entity<stack_frame_list::StackFrameList>,
 46    _module_list: Entity<module_list::ModuleList>,
 47    _console: Entity<Console>,
 48    panes: PaneGroup,
 49    pane_close_subscriptions: HashMap<EntityId, Subscription>,
 50}
 51
 52impl Render for RunningState {
 53    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 54        let active = self.panes.panes().into_iter().next();
 55        let x = if let Some(active) = active {
 56            self.panes
 57                .render(
 58                    None,
 59                    &ActivePaneDecorator::new(active, &self.workspace),
 60                    window,
 61                    cx,
 62                )
 63                .into_any_element()
 64        } else {
 65            div().into_any_element()
 66        };
 67        let thread_status = self
 68            .thread_id
 69            .map(|thread_id| self.session.read(cx).thread_status(thread_id))
 70            .unwrap_or(ThreadStatus::Exited);
 71
 72        self.variable_list.update(cx, |this, cx| {
 73            this.disabled(thread_status != ThreadStatus::Stopped, cx);
 74        });
 75        v_flex()
 76            .size_full()
 77            .key_context("DebugSessionItem")
 78            .track_focus(&self.focus_handle(cx))
 79            .child(h_flex().flex_1().child(x))
 80    }
 81}
 82
 83struct SubView {
 84    inner: AnyView,
 85    pane_focus_handle: FocusHandle,
 86    tab_name: SharedString,
 87}
 88
 89impl SubView {
 90    fn new(
 91        pane_focus_handle: FocusHandle,
 92        view: AnyView,
 93        tab_name: SharedString,
 94        cx: &mut App,
 95    ) -> Entity<Self> {
 96        cx.new(|_| Self {
 97            tab_name,
 98            inner: view,
 99            pane_focus_handle,
100        })
101    }
102}
103impl Focusable for SubView {
104    fn focus_handle(&self, _: &App) -> FocusHandle {
105        self.pane_focus_handle.clone()
106    }
107}
108impl EventEmitter<()> for SubView {}
109impl Item for SubView {
110    type Event = ();
111    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
112        Some(self.tab_name.clone())
113    }
114}
115
116impl Render for SubView {
117    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
118        v_flex().size_full().child(self.inner.clone())
119    }
120}
121
122fn new_debugger_pane(
123    workspace: WeakEntity<Workspace>,
124    project: Entity<Project>,
125    window: &mut Window,
126    cx: &mut Context<RunningState>,
127) -> Entity<Pane> {
128    let weak_running = cx.weak_entity();
129    let custom_drop_handle = {
130        let workspace = workspace.clone();
131        let project = project.downgrade();
132        let weak_running = weak_running.clone();
133        move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
134            let Some(tab) = any.downcast_ref::<DraggedTab>() else {
135                return ControlFlow::Break(());
136            };
137            let Some(project) = project.upgrade() else {
138                return ControlFlow::Break(());
139            };
140            let this_pane = cx.entity().clone();
141            let item = if tab.pane == this_pane {
142                pane.item_for_index(tab.ix)
143            } else {
144                tab.pane.read(cx).item_for_index(tab.ix)
145            };
146            let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
147                return ControlFlow::Break(());
148            };
149
150            let source = tab.pane.clone();
151            let item_id_to_move = item.item_id();
152
153            let Ok(new_split_pane) = pane
154                .drag_split_direction()
155                .map(|split_direction| {
156                    weak_running.update(cx, |running, cx| {
157                        let new_pane =
158                            new_debugger_pane(workspace.clone(), project.clone(), window, cx);
159                        let _previous_subscription = running.pane_close_subscriptions.insert(
160                            new_pane.entity_id(),
161                            cx.subscribe(&new_pane, RunningState::handle_pane_event),
162                        );
163                        debug_assert!(_previous_subscription.is_none());
164                        running
165                            .panes
166                            .split(&this_pane, &new_pane, split_direction)?;
167                        anyhow::Ok(new_pane)
168                    })
169                })
170                .transpose()
171            else {
172                return ControlFlow::Break(());
173            };
174
175            match new_split_pane.transpose() {
176                // Source pane may be the one currently updated, so defer the move.
177                Ok(Some(new_pane)) => cx
178                    .spawn_in(window, async move |_, cx| {
179                        cx.update(|window, cx| {
180                            move_item(
181                                &source,
182                                &new_pane,
183                                item_id_to_move,
184                                new_pane.read(cx).active_item_index(),
185                                window,
186                                cx,
187                            );
188                        })
189                        .ok();
190                    })
191                    .detach(),
192                // If we drop into existing pane or current pane,
193                // regular pane drop handler will take care of it,
194                // using the right tab index for the operation.
195                Ok(None) => return ControlFlow::Continue(()),
196                err @ Err(_) => {
197                    err.log_err();
198                    return ControlFlow::Break(());
199                }
200            };
201
202            ControlFlow::Break(())
203        }
204    };
205
206    let ret = cx.new(move |cx| {
207        let mut pane = Pane::new(
208            workspace.clone(),
209            project.clone(),
210            Default::default(),
211            None,
212            NoAction.boxed_clone(),
213            window,
214            cx,
215        );
216        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
217            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
218                let is_current_pane = tab.pane == cx.entity();
219                let Some(can_drag_away) = weak_running
220                    .update(cx, |running_state, _| {
221                        let current_panes = running_state.panes.panes();
222                        !current_panes.contains(&&tab.pane)
223                            || current_panes.len() > 1
224                            || (!is_current_pane || pane.items_len() > 1)
225                    })
226                    .ok()
227                else {
228                    return false;
229                };
230                if can_drag_away {
231                    let item = if is_current_pane {
232                        pane.item_for_index(tab.ix)
233                    } else {
234                        tab.pane.read(cx).item_for_index(tab.ix)
235                    };
236                    if let Some(item) = item {
237                        return item.downcast::<SubView>().is_some();
238                    }
239                }
240            }
241            false
242        })));
243        pane.display_nav_history_buttons(None);
244        pane.set_custom_drop_handle(cx, custom_drop_handle);
245        pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
246        pane
247    });
248
249    ret
250}
251impl RunningState {
252    pub fn new(
253        session: Entity<Session>,
254        project: Entity<Project>,
255        workspace: WeakEntity<Workspace>,
256        window: &mut Window,
257        cx: &mut Context<Self>,
258    ) -> Self {
259        let focus_handle = cx.focus_handle();
260        let session_id = session.read(cx).session_id();
261        let weak_state = cx.weak_entity();
262        let stack_frame_list = cx.new(|cx| {
263            StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
264        });
265
266        let variable_list =
267            cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
268
269        let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
270
271        #[expect(unused)]
272        let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
273
274        let console = cx.new(|cx| {
275            Console::new(
276                session.clone(),
277                stack_frame_list.clone(),
278                variable_list.clone(),
279                window,
280                cx,
281            )
282        });
283
284        let _subscriptions = vec![
285            cx.observe(&module_list, |_, _, cx| cx.notify()),
286            cx.subscribe_in(&session, window, |this, _, event, window, cx| {
287                match event {
288                    SessionEvent::Stopped(thread_id) => {
289                        this.workspace
290                            .update(cx, |workspace, cx| {
291                                workspace.open_panel::<crate::DebugPanel>(window, cx);
292                            })
293                            .log_err();
294
295                        if let Some(thread_id) = thread_id {
296                            this.select_thread(*thread_id, cx);
297                        }
298                    }
299                    SessionEvent::Threads => {
300                        let threads = this.session.update(cx, |this, cx| this.threads(cx));
301                        this.select_current_thread(&threads, cx);
302                    }
303                    _ => {}
304                }
305                cx.notify()
306            }),
307        ];
308
309        let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
310        leftmost_pane.update(cx, |this, cx| {
311            this.add_item(
312                Box::new(SubView::new(
313                    this.focus_handle(cx),
314                    stack_frame_list.clone().into(),
315                    SharedString::new_static("Frames"),
316                    cx,
317                )),
318                true,
319                false,
320                None,
321                window,
322                cx,
323            );
324        });
325        let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
326        center_pane.update(cx, |this, cx| {
327            this.add_item(
328                Box::new(SubView::new(
329                    variable_list.focus_handle(cx),
330                    variable_list.clone().into(),
331                    SharedString::new_static("Variables"),
332                    cx,
333                )),
334                true,
335                false,
336                None,
337                window,
338                cx,
339            );
340            this.add_item(
341                Box::new(SubView::new(
342                    this.focus_handle(cx),
343                    module_list.clone().into(),
344                    SharedString::new_static("Modules"),
345                    cx,
346                )),
347                false,
348                false,
349                None,
350                window,
351                cx,
352            );
353            this.activate_item(0, false, false, window, cx);
354        });
355        let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
356        rightmost_pane.update(cx, |this, cx| {
357            this.add_item(
358                Box::new(SubView::new(
359                    this.focus_handle(cx),
360                    console.clone().into(),
361                    SharedString::new_static("Console"),
362                    cx,
363                )),
364                true,
365                false,
366                None,
367                window,
368                cx,
369            );
370        });
371        let pane_close_subscriptions = HashMap::from_iter(
372            [&leftmost_pane, &center_pane, &rightmost_pane]
373                .into_iter()
374                .map(|entity| {
375                    (
376                        entity.entity_id(),
377                        cx.subscribe(entity, Self::handle_pane_event),
378                    )
379                }),
380        );
381        let group_root = workspace::PaneAxis::new(
382            gpui::Axis::Horizontal,
383            [leftmost_pane, center_pane, rightmost_pane]
384                .into_iter()
385                .map(workspace::Member::Pane)
386                .collect(),
387        );
388
389        let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
390
391        Self {
392            session,
393            workspace,
394            focus_handle,
395            variable_list,
396            _subscriptions,
397            thread_id: None,
398            _remote_id: None,
399            stack_frame_list,
400            session_id,
401            panes,
402            _module_list: module_list,
403            _console: console,
404            pane_close_subscriptions,
405        }
406    }
407
408    fn handle_pane_event(
409        this: &mut RunningState,
410        source_pane: Entity<Pane>,
411        event: &Event,
412        cx: &mut Context<RunningState>,
413    ) {
414        if let Event::Remove { .. } = event {
415            let _did_find_pane = this.panes.remove(&source_pane).is_ok();
416            debug_assert!(_did_find_pane);
417            cx.notify();
418        }
419    }
420    pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
421        if self.thread_id.is_some() {
422            self.stack_frame_list
423                .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
424        }
425    }
426
427    pub fn session(&self) -> &Entity<Session> {
428        &self.session
429    }
430
431    pub fn session_id(&self) -> SessionId {
432        self.session_id
433    }
434
435    #[cfg(test)]
436    pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
437        &self.stack_frame_list
438    }
439
440    #[cfg(test)]
441    pub fn console(&self) -> &Entity<Console> {
442        &self._console
443    }
444
445    #[cfg(test)]
446    pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
447        &self._module_list
448    }
449
450    #[cfg(test)]
451    pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
452        let (variable_list_position, pane) = self
453            .panes
454            .panes()
455            .into_iter()
456            .find_map(|pane| {
457                pane.read(cx)
458                    .items_of_type::<SubView>()
459                    .position(|view| view.read(cx).tab_name == *"Modules")
460                    .map(|view| (view, pane))
461            })
462            .unwrap();
463        pane.update(cx, |this, cx| {
464            this.activate_item(variable_list_position, true, true, window, cx);
465        })
466    }
467    #[cfg(test)]
468    pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
469        &self.variable_list
470    }
471
472    pub fn capabilities(&self, cx: &App) -> Capabilities {
473        self.session().read(cx).capabilities().clone()
474    }
475
476    pub fn select_current_thread(
477        &mut self,
478        threads: &Vec<(Thread, ThreadStatus)>,
479        cx: &mut Context<Self>,
480    ) {
481        let selected_thread = self
482            .thread_id
483            .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
484            .or_else(|| threads.first());
485
486        let Some((selected_thread, _)) = selected_thread else {
487            return;
488        };
489
490        if Some(ThreadId(selected_thread.id)) != self.thread_id {
491            self.select_thread(ThreadId(selected_thread.id), cx);
492        }
493    }
494
495    #[cfg(test)]
496    pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
497        self.thread_id
498    }
499
500    pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
501        self.thread_id
502            .map(|id| self.session().read(cx).thread_status(id))
503    }
504
505    fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
506        if self.thread_id.is_some_and(|id| id == thread_id) {
507            return;
508        }
509
510        self.thread_id = Some(thread_id);
511
512        self.stack_frame_list
513            .update(cx, |list, cx| list.refresh(cx));
514        cx.notify();
515    }
516
517    pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
518        let Some(thread_id) = self.thread_id else {
519            return;
520        };
521
522        self.session().update(cx, |state, cx| {
523            state.continue_thread(thread_id, cx);
524        });
525    }
526
527    pub fn step_over(&mut self, cx: &mut Context<Self>) {
528        let Some(thread_id) = self.thread_id else {
529            return;
530        };
531
532        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
533
534        self.session().update(cx, |state, cx| {
535            state.step_over(thread_id, granularity, cx);
536        });
537    }
538
539    pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
540        let Some(thread_id) = self.thread_id else {
541            return;
542        };
543
544        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
545
546        self.session().update(cx, |state, cx| {
547            state.step_in(thread_id, granularity, cx);
548        });
549    }
550
551    pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
552        let Some(thread_id) = self.thread_id else {
553            return;
554        };
555
556        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
557
558        self.session().update(cx, |state, cx| {
559            state.step_out(thread_id, granularity, cx);
560        });
561    }
562
563    pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
564        let Some(thread_id) = self.thread_id else {
565            return;
566        };
567
568        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
569
570        self.session().update(cx, |state, cx| {
571            state.step_back(thread_id, granularity, cx);
572        });
573    }
574
575    pub fn restart_session(&self, cx: &mut Context<Self>) {
576        self.session().update(cx, |state, cx| {
577            state.restart(None, cx);
578        });
579    }
580
581    pub fn pause_thread(&self, cx: &mut Context<Self>) {
582        let Some(thread_id) = self.thread_id else {
583            return;
584        };
585
586        self.session().update(cx, |state, cx| {
587            state.pause_thread(thread_id, cx);
588        });
589    }
590
591    pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
592        self.workspace
593            .update(cx, |workspace, cx| {
594                workspace
595                    .project()
596                    .read(cx)
597                    .breakpoint_store()
598                    .update(cx, |store, cx| {
599                        store.remove_active_position(Some(self.session_id), cx)
600                    })
601            })
602            .log_err();
603
604        self.session.update(cx, |session, cx| {
605            session.shutdown(cx).detach();
606        })
607    }
608
609    pub fn stop_thread(&self, cx: &mut Context<Self>) {
610        let Some(thread_id) = self.thread_id else {
611            return;
612        };
613
614        self.workspace
615            .update(cx, |workspace, cx| {
616                workspace
617                    .project()
618                    .read(cx)
619                    .breakpoint_store()
620                    .update(cx, |store, cx| {
621                        store.remove_active_position(Some(self.session_id), cx)
622                    })
623            })
624            .log_err();
625
626        self.session().update(cx, |state, cx| {
627            state.terminate_threads(Some(vec![thread_id; 1]), cx);
628        });
629    }
630
631    #[expect(
632        unused,
633        reason = "Support for disconnecting a client is not wired through yet"
634    )]
635    pub fn disconnect_client(&self, cx: &mut Context<Self>) {
636        self.session().update(cx, |state, cx| {
637            state.disconnect_client(cx);
638        });
639    }
640
641    pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
642        self.session.update(cx, |session, cx| {
643            session.toggle_ignore_breakpoints(cx).detach();
644        });
645    }
646
647    pub(crate) fn thread_dropdown(
648        &self,
649        window: &mut Window,
650        cx: &mut Context<'_, RunningState>,
651    ) -> DropdownMenu {
652        let state = cx.entity();
653        let threads = self.session.update(cx, |this, cx| this.threads(cx));
654        let selected_thread_name = threads
655            .iter()
656            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
657            .map(|(thread, _)| thread.name.clone())
658            .unwrap_or("Threads".to_owned());
659        DropdownMenu::new(
660            ("thread-list", self.session_id.0),
661            selected_thread_name,
662            ContextMenu::build(window, cx, move |mut this, _, _| {
663                for (thread, _) in threads {
664                    let state = state.clone();
665                    let thread_id = thread.id;
666                    this = this.entry(thread.name, None, move |_, cx| {
667                        state.update(cx, |state, cx| {
668                            state.select_thread(ThreadId(thread_id), cx);
669                        });
670                    });
671                }
672                this
673            }),
674        )
675    }
676}
677
678impl EventEmitter<DebugPanelItemEvent> for RunningState {}
679
680impl Focusable for RunningState {
681    fn focus_handle(&self, _: &App) -> FocusHandle {
682        self.focus_handle.clone()
683    }
684}