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
246    });
247
248    ret
249}
250impl RunningState {
251    pub fn new(
252        session: Entity<Session>,
253        project: Entity<Project>,
254        workspace: WeakEntity<Workspace>,
255        window: &mut Window,
256        cx: &mut Context<Self>,
257    ) -> Self {
258        let focus_handle = cx.focus_handle();
259        let session_id = session.read(cx).session_id();
260        let weak_state = cx.weak_entity();
261        let stack_frame_list = cx.new(|cx| {
262            StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
263        });
264
265        let variable_list =
266            cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
267
268        let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
269
270        #[expect(unused)]
271        let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
272
273        let console = cx.new(|cx| {
274            Console::new(
275                session.clone(),
276                stack_frame_list.clone(),
277                variable_list.clone(),
278                window,
279                cx,
280            )
281        });
282
283        let _subscriptions = vec![
284            cx.observe(&module_list, |_, _, cx| cx.notify()),
285            cx.subscribe_in(&session, window, |this, _, event, window, cx| {
286                match event {
287                    SessionEvent::Stopped(thread_id) => {
288                        this.workspace
289                            .update(cx, |workspace, cx| {
290                                workspace.open_panel::<crate::DebugPanel>(window, cx);
291                            })
292                            .log_err();
293
294                        if let Some(thread_id) = thread_id {
295                            this.select_thread(*thread_id, cx);
296                        }
297                    }
298                    SessionEvent::Threads => {
299                        let threads = this.session.update(cx, |this, cx| this.threads(cx));
300                        this.select_current_thread(&threads, cx);
301                    }
302                    _ => {}
303                }
304                cx.notify()
305            }),
306        ];
307
308        let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
309        leftmost_pane.update(cx, |this, cx| {
310            this.add_item(
311                Box::new(SubView::new(
312                    this.focus_handle(cx),
313                    stack_frame_list.clone().into(),
314                    SharedString::new_static("Frames"),
315                    cx,
316                )),
317                true,
318                false,
319                None,
320                window,
321                cx,
322            );
323        });
324        let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
325        center_pane.update(cx, |this, cx| {
326            this.add_item(
327                Box::new(SubView::new(
328                    variable_list.focus_handle(cx),
329                    variable_list.clone().into(),
330                    SharedString::new_static("Variables"),
331                    cx,
332                )),
333                true,
334                false,
335                None,
336                window,
337                cx,
338            );
339            this.add_item(
340                Box::new(SubView::new(
341                    this.focus_handle(cx),
342                    module_list.clone().into(),
343                    SharedString::new_static("Modules"),
344                    cx,
345                )),
346                true,
347                false,
348                None,
349                window,
350                cx,
351            );
352        });
353        let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
354        rightmost_pane.update(cx, |this, cx| {
355            this.add_item(
356                Box::new(SubView::new(
357                    this.focus_handle(cx),
358                    console.clone().into(),
359                    SharedString::new_static("Console"),
360                    cx,
361                )),
362                true,
363                false,
364                None,
365                window,
366                cx,
367            );
368        });
369        let pane_close_subscriptions = HashMap::from_iter(
370            [&leftmost_pane, &center_pane, &rightmost_pane]
371                .into_iter()
372                .map(|entity| {
373                    (
374                        entity.entity_id(),
375                        cx.subscribe(entity, Self::handle_pane_event),
376                    )
377                }),
378        );
379        let group_root = workspace::PaneAxis::new(
380            gpui::Axis::Horizontal,
381            [leftmost_pane, center_pane, rightmost_pane]
382                .into_iter()
383                .map(workspace::Member::Pane)
384                .collect(),
385        );
386
387        let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
388
389        Self {
390            session,
391            workspace,
392            focus_handle,
393            variable_list,
394            _subscriptions,
395            thread_id: None,
396            _remote_id: None,
397            stack_frame_list,
398            session_id,
399            panes,
400            _module_list: module_list,
401            _console: console,
402            pane_close_subscriptions,
403        }
404    }
405
406    fn handle_pane_event(
407        this: &mut RunningState,
408        source_pane: Entity<Pane>,
409        event: &Event,
410        cx: &mut Context<RunningState>,
411    ) {
412        if let Event::Remove { .. } = event {
413            let _did_find_pane = this.panes.remove(&source_pane).is_ok();
414            debug_assert!(_did_find_pane);
415            cx.notify();
416        }
417    }
418    pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
419        if self.thread_id.is_some() {
420            self.stack_frame_list
421                .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
422        }
423    }
424
425    pub fn session(&self) -> &Entity<Session> {
426        &self.session
427    }
428
429    pub fn session_id(&self) -> SessionId {
430        self.session_id
431    }
432
433    #[cfg(test)]
434    pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
435        &self.stack_frame_list
436    }
437
438    #[cfg(test)]
439    pub fn console(&self) -> &Entity<Console> {
440        &self._console
441    }
442
443    #[cfg(test)]
444    pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
445        &self._module_list
446    }
447
448    #[cfg(test)]
449    pub(crate) fn activate_variable_list(&self, window: &mut Window, cx: &mut App) {
450        let (variable_list_position, pane) = self
451            .panes
452            .panes()
453            .into_iter()
454            .find_map(|pane| {
455                pane.read(cx)
456                    .items_of_type::<SubView>()
457                    .position(|view| view.read(cx).tab_name == *"Variables")
458                    .map(|view| (view, pane))
459            })
460            .unwrap();
461        pane.update(cx, |this, cx| {
462            this.activate_item(variable_list_position, true, true, window, cx);
463        })
464    }
465    #[cfg(test)]
466    pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
467        &self.variable_list
468    }
469
470    pub fn capabilities(&self, cx: &App) -> Capabilities {
471        self.session().read(cx).capabilities().clone()
472    }
473
474    pub fn select_current_thread(
475        &mut self,
476        threads: &Vec<(Thread, ThreadStatus)>,
477        cx: &mut Context<Self>,
478    ) {
479        let selected_thread = self
480            .thread_id
481            .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
482            .or_else(|| threads.first());
483
484        let Some((selected_thread, _)) = selected_thread else {
485            return;
486        };
487
488        if Some(ThreadId(selected_thread.id)) != self.thread_id {
489            self.select_thread(ThreadId(selected_thread.id), cx);
490        }
491    }
492
493    #[cfg(test)]
494    pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
495        self.thread_id
496    }
497
498    pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
499        self.thread_id
500            .map(|id| self.session().read(cx).thread_status(id))
501    }
502
503    fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
504        if self.thread_id.is_some_and(|id| id == thread_id) {
505            return;
506        }
507
508        self.thread_id = Some(thread_id);
509
510        self.stack_frame_list
511            .update(cx, |list, cx| list.refresh(cx));
512        cx.notify();
513    }
514
515    pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
516        let Some(thread_id) = self.thread_id else {
517            return;
518        };
519
520        self.session().update(cx, |state, cx| {
521            state.continue_thread(thread_id, cx);
522        });
523    }
524
525    pub fn step_over(&mut self, cx: &mut Context<Self>) {
526        let Some(thread_id) = self.thread_id else {
527            return;
528        };
529
530        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
531
532        self.session().update(cx, |state, cx| {
533            state.step_over(thread_id, granularity, cx);
534        });
535    }
536
537    pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
538        let Some(thread_id) = self.thread_id else {
539            return;
540        };
541
542        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
543
544        self.session().update(cx, |state, cx| {
545            state.step_in(thread_id, granularity, cx);
546        });
547    }
548
549    pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
550        let Some(thread_id) = self.thread_id else {
551            return;
552        };
553
554        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
555
556        self.session().update(cx, |state, cx| {
557            state.step_out(thread_id, granularity, cx);
558        });
559    }
560
561    pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
562        let Some(thread_id) = self.thread_id else {
563            return;
564        };
565
566        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
567
568        self.session().update(cx, |state, cx| {
569            state.step_back(thread_id, granularity, cx);
570        });
571    }
572
573    pub fn restart_session(&self, cx: &mut Context<Self>) {
574        self.session().update(cx, |state, cx| {
575            state.restart(None, cx);
576        });
577    }
578
579    pub fn pause_thread(&self, cx: &mut Context<Self>) {
580        let Some(thread_id) = self.thread_id else {
581            return;
582        };
583
584        self.session().update(cx, |state, cx| {
585            state.pause_thread(thread_id, cx);
586        });
587    }
588
589    pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
590        self.workspace
591            .update(cx, |workspace, cx| {
592                workspace
593                    .project()
594                    .read(cx)
595                    .breakpoint_store()
596                    .update(cx, |store, cx| {
597                        store.remove_active_position(Some(self.session_id), cx)
598                    })
599            })
600            .log_err();
601
602        self.session.update(cx, |session, cx| {
603            session.shutdown(cx).detach();
604        })
605    }
606
607    pub fn stop_thread(&self, cx: &mut Context<Self>) {
608        let Some(thread_id) = self.thread_id else {
609            return;
610        };
611
612        self.workspace
613            .update(cx, |workspace, cx| {
614                workspace
615                    .project()
616                    .read(cx)
617                    .breakpoint_store()
618                    .update(cx, |store, cx| {
619                        store.remove_active_position(Some(self.session_id), cx)
620                    })
621            })
622            .log_err();
623
624        self.session().update(cx, |state, cx| {
625            state.terminate_threads(Some(vec![thread_id; 1]), cx);
626        });
627    }
628
629    #[expect(
630        unused,
631        reason = "Support for disconnecting a client is not wired through yet"
632    )]
633    pub fn disconnect_client(&self, cx: &mut Context<Self>) {
634        self.session().update(cx, |state, cx| {
635            state.disconnect_client(cx);
636        });
637    }
638
639    pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
640        self.session.update(cx, |session, cx| {
641            session.toggle_ignore_breakpoints(cx).detach();
642        });
643    }
644
645    pub(crate) fn thread_dropdown(
646        &self,
647        window: &mut Window,
648        cx: &mut Context<'_, RunningState>,
649    ) -> DropdownMenu {
650        let state = cx.entity();
651        let threads = self.session.update(cx, |this, cx| this.threads(cx));
652        let selected_thread_name = threads
653            .iter()
654            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
655            .map(|(thread, _)| thread.name.clone())
656            .unwrap_or("Threads".to_owned());
657        DropdownMenu::new(
658            ("thread-list", self.session_id.0),
659            selected_thread_name,
660            ContextMenu::build(window, cx, move |mut this, _, _| {
661                for (thread, _) in threads {
662                    let state = state.clone();
663                    let thread_id = thread.id;
664                    this = this.entry(thread.name, None, move |_, cx| {
665                        state.update(cx, |state, cx| {
666                            state.select_thread(ThreadId(thread_id), cx);
667                        });
668                    });
669                }
670                this
671            }),
672        )
673    }
674}
675
676impl EventEmitter<DebugPanelItemEvent> for RunningState {}
677
678impl Focusable for RunningState {
679    fn focus_handle(&self, _: &App) -> FocusHandle {
680        self.focus_handle.clone()
681    }
682}