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    pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
436        self.stack_frame_list.read(cx).selected_stack_frame_id()
437    }
438
439    #[cfg(test)]
440    pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
441        &self.stack_frame_list
442    }
443
444    #[cfg(test)]
445    pub fn console(&self) -> &Entity<Console> {
446        &self._console
447    }
448
449    #[cfg(test)]
450    pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
451        &self._module_list
452    }
453
454    #[cfg(test)]
455    pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
456        let (variable_list_position, pane) = self
457            .panes
458            .panes()
459            .into_iter()
460            .find_map(|pane| {
461                pane.read(cx)
462                    .items_of_type::<SubView>()
463                    .position(|view| view.read(cx).tab_name == *"Modules")
464                    .map(|view| (view, pane))
465            })
466            .unwrap();
467        pane.update(cx, |this, cx| {
468            this.activate_item(variable_list_position, true, true, window, cx);
469        })
470    }
471    #[cfg(test)]
472    pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
473        &self.variable_list
474    }
475
476    pub fn capabilities(&self, cx: &App) -> Capabilities {
477        self.session().read(cx).capabilities().clone()
478    }
479
480    pub fn select_current_thread(
481        &mut self,
482        threads: &Vec<(Thread, ThreadStatus)>,
483        cx: &mut Context<Self>,
484    ) {
485        let selected_thread = self
486            .thread_id
487            .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
488            .or_else(|| threads.first());
489
490        let Some((selected_thread, _)) = selected_thread else {
491            return;
492        };
493
494        if Some(ThreadId(selected_thread.id)) != self.thread_id {
495            self.select_thread(ThreadId(selected_thread.id), cx);
496        }
497    }
498
499    pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
500        self.thread_id
501    }
502
503    pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
504        self.thread_id
505            .map(|id| self.session().read(cx).thread_status(id))
506    }
507
508    fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
509        if self.thread_id.is_some_and(|id| id == thread_id) {
510            return;
511        }
512
513        self.thread_id = Some(thread_id);
514
515        self.stack_frame_list
516            .update(cx, |list, cx| list.refresh(cx));
517        cx.notify();
518    }
519
520    pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
521        let Some(thread_id) = self.thread_id else {
522            return;
523        };
524
525        self.session().update(cx, |state, cx| {
526            state.continue_thread(thread_id, cx);
527        });
528    }
529
530    pub fn step_over(&mut self, cx: &mut Context<Self>) {
531        let Some(thread_id) = self.thread_id else {
532            return;
533        };
534
535        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
536
537        self.session().update(cx, |state, cx| {
538            state.step_over(thread_id, granularity, cx);
539        });
540    }
541
542    pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
543        let Some(thread_id) = self.thread_id else {
544            return;
545        };
546
547        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
548
549        self.session().update(cx, |state, cx| {
550            state.step_in(thread_id, granularity, cx);
551        });
552    }
553
554    pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
555        let Some(thread_id) = self.thread_id else {
556            return;
557        };
558
559        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
560
561        self.session().update(cx, |state, cx| {
562            state.step_out(thread_id, granularity, cx);
563        });
564    }
565
566    pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
567        let Some(thread_id) = self.thread_id else {
568            return;
569        };
570
571        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
572
573        self.session().update(cx, |state, cx| {
574            state.step_back(thread_id, granularity, cx);
575        });
576    }
577
578    pub fn restart_session(&self, cx: &mut Context<Self>) {
579        self.session().update(cx, |state, cx| {
580            state.restart(None, cx);
581        });
582    }
583
584    pub fn pause_thread(&self, cx: &mut Context<Self>) {
585        let Some(thread_id) = self.thread_id else {
586            return;
587        };
588
589        self.session().update(cx, |state, cx| {
590            state.pause_thread(thread_id, cx);
591        });
592    }
593
594    pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
595        self.workspace
596            .update(cx, |workspace, cx| {
597                workspace
598                    .project()
599                    .read(cx)
600                    .breakpoint_store()
601                    .update(cx, |store, cx| {
602                        store.remove_active_position(Some(self.session_id), cx)
603                    })
604            })
605            .log_err();
606
607        self.session.update(cx, |session, cx| {
608            session.shutdown(cx).detach();
609        })
610    }
611
612    pub fn stop_thread(&self, cx: &mut Context<Self>) {
613        let Some(thread_id) = self.thread_id else {
614            return;
615        };
616
617        self.workspace
618            .update(cx, |workspace, cx| {
619                workspace
620                    .project()
621                    .read(cx)
622                    .breakpoint_store()
623                    .update(cx, |store, cx| {
624                        store.remove_active_position(Some(self.session_id), cx)
625                    })
626            })
627            .log_err();
628
629        self.session().update(cx, |state, cx| {
630            state.terminate_threads(Some(vec![thread_id; 1]), cx);
631        });
632    }
633
634    #[expect(
635        unused,
636        reason = "Support for disconnecting a client is not wired through yet"
637    )]
638    pub fn disconnect_client(&self, cx: &mut Context<Self>) {
639        self.session().update(cx, |state, cx| {
640            state.disconnect_client(cx);
641        });
642    }
643
644    pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
645        self.session.update(cx, |session, cx| {
646            session.toggle_ignore_breakpoints(cx).detach();
647        });
648    }
649
650    pub(crate) fn thread_dropdown(
651        &self,
652        window: &mut Window,
653        cx: &mut Context<'_, RunningState>,
654    ) -> DropdownMenu {
655        let state = cx.entity();
656        let threads = self.session.update(cx, |this, cx| this.threads(cx));
657        let selected_thread_name = threads
658            .iter()
659            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
660            .map(|(thread, _)| thread.name.clone())
661            .unwrap_or("Threads".to_owned());
662        DropdownMenu::new(
663            ("thread-list", self.session_id.0),
664            selected_thread_name,
665            ContextMenu::build(window, cx, move |mut this, _, _| {
666                for (thread, _) in threads {
667                    let state = state.clone();
668                    let thread_id = thread.id;
669                    this = this.entry(thread.name, None, move |_, cx| {
670                        state.update(cx, |state, cx| {
671                            state.select_thread(ThreadId(thread_id), cx);
672                        });
673                    });
674                }
675                this
676            }),
677        )
678    }
679}
680
681impl EventEmitter<DebugPanelItemEvent> for RunningState {}
682
683impl Focusable for RunningState {
684    fn focus_handle(&self, _: &App) -> FocusHandle {
685        self.focus_handle.clone()
686    }
687}